Android 元件化最佳實踐

singwhatiwanna發表於2018-07-31

本文由玉剛說寫作平臺提供寫作贊助

原作者:任雪龍

版權宣告:本文版權歸微信公眾號玉剛說所有,未經許可,不得以任何形式轉載

Demo地址
https://github.com/renxuelong/ComponentDemo

演示為先

Android 元件化最佳實踐

在專案的開發過程中,隨著開發人員的增多及功能的增加,如果提前沒有使用合理的開發架構,那麼程式碼會越來臃腫,功能間程式碼耦合也會越來越嚴重,這時候為了保證專案程式碼的質量,我們就必須進行重構。

比較簡單的開發架構是按照功能模組進行拆分,也就是用 Android 開發中的 module 這個概念,每個功能都是一個 module,每個功能的程式碼都在自己所屬的 module 中新增。這樣的設計在各個功能相互直接比較獨立的情況下是比較合理的,但是當多個模組中涉及到相同功能時程式碼的耦合又會增加。

例如首頁模組和直播間模組中都可能涉及到了視訊播放的功能,這時候不管將播放控制的程式碼放到首頁還是直播間,開發過程中都會發現,我們想要解決的程式碼耦合情況又又又又出現了。為了進一步解決這個問題,元件化的開發模式順勢而來。

一、元件化和模組化的區別

上面說到了從普通的無架構到模組化,再由模組化到元件化,那麼其中的界限是什麼,模組化和元件化的本質區別又是什麼?為了解決這些問題,我們就要先了解 “模組” 和 “元件” 的區別。

模組

模組指的是獨立的業務模組,比如剛才提到的 [首頁模組]、[直播間模組] 等。

元件

元件指的是單一的功能元件,如 [視訊元件]、[支付元件] 等,每個元件都可以以一個單獨的 module 開發,並且可以單獨抽出來作為 SDK 對外發布使用。

由此來看,[模組] 和 [元件] 間最明顯的區別就是模組相對與元件來說粒度更大,一個模組中可能包含多個元件。並且兩種方式的本質思想是一樣的,都是為了程式碼重用和業務解耦。在劃分的時候,模組化是業務導向,元件化是功能導向。

元件化基礎架構圖

上面是一個非常基礎的元件化架構圖,圖中從上向下分別為應用層、元件層和基礎層。

基礎層: 基礎層很容易理解,其中包含的是一些基礎庫以及對基礎庫的封裝,比如常用的圖片載入,網路請求,資料儲存操作等等,其他模組或者元件都可以引用同一套基礎庫,這樣不但只需要開發一套程式碼,還解耦了基礎功能和業務功能的耦合,在基礎庫變更時更加容易操作。

元件層: 基礎層往上是元件層,元件層就包含一些簡單的功能元件,比如視訊,支付等等

應用層: 元件層往上是應用層,這裡為了簡單,只新增了一個 APP ,APP 就相當於我們的模組,一個具體的業務模組會按需引用不同的元件,最終實現業務功能,這裡如果又多個業務模組,就可以各自按需引用元件,最後將各個模組統籌輸出 APP。

到這裡我們最簡單的元件化架構就已經可以使用了,但是這只是最理想的狀態下的架構,實際的開發中,不同的元件不可能徹底的相互隔離,元件中肯定會有相互傳遞資料、呼叫方法、頁面跳轉等情況。

比如直播元件中使用者需要刷禮物,刷禮物就需要支付元件的支援,而支付元件中支付操作是必須需要登入狀態、使用者 ID 等資訊。如果當前未登入,是需要先跳轉到登入元件中進行登入操作,登入成功後才能正常的進行支付流程。

而我們上面的架構圖中,各個元件之間是相互隔離的,沒有相互依賴,如果想直接進行元件互動,也就是元件間相互依賴,這就又違背了元件化開發的規則。所以我們必須找到方法解決這些問題才能進行元件化開發。

二、元件化開發需要解決的問題

在實現元件化的過程中,同一個問題可能有不同的技術路徑可以解決,但是需要解決的問題主要有以下幾點:

  1. 每個元件都是一個完整的整體,所以元件開發過程中要滿足單獨執行及除錯的要求,這樣還可以提升開發過程中專案的編譯速度。

  2. 資料傳遞與元件間方法的相互呼叫,這也是上面我們提到的一個必須要解決的問題。

  3. 元件間介面跳轉,不同元件之間不僅會有資料的傳遞,也會有相互的頁面跳轉。在元件化開發過程中如何在不相互依賴的情況下實現互相跳轉?

  4. 主專案不直接訪問元件中具體類的情況下,如何獲取元件中 Fragment 的例項並將元件中的 Fragment 例項新增到主專案的介面中?

  5. 元件開發完成後相互之間的整合除錯如何實現?還有就是在整合除錯階段,依賴多個元件進行開發時,如果實現只依賴部分元件時可以編譯通過?這樣也會降低編譯時間,提升效率。

  6. 元件解耦的目標以及如何實現程式碼隔離?不僅元件之間相互隔離,還有第五個問題中模組依賴元件時可以動態增刪元件,這樣就是模組不會對元件中特定的類進行操作,所以完全的隔絕模組對元件中類的使用會使解耦更加徹底,程式也更加健壯。

以上就是實現元件化的過程中我們要解決的主要問題,下面我們會一個一個來解決,最終實現比較合理的元件化開發。

三、元件單獨除錯

1. 動態配置元件的工程型別?

在 AndroidStudio 開發 Android 專案時,使用的是 Gradle 來構建,具體來說使用的是 Android Gradle 外掛來構建,Android Gradle 中提供了三種外掛,在開發中可以通過配置不同的外掛來配置不同的工程。

  • App 外掛,id: com.android.application
  • Library 外掛,id: com.android.libraay
  • Test 外掛,id: com.android.test

區別比較簡單, App 外掛來配置一個 Android App 工程,專案構建後輸出一個 APK 安裝包,Library 外掛來配置一個 Android Library 工程,構建後輸出 aar 包,Test 外掛來配置一個 Android Test 工程。我們這裡主要使用 App 外掛和 Library 外掛來實現元件的單獨除錯。這裡就出現了第一個小問題,如何動態配置元件的工程型別?

通過工程的 build.gradle 檔案中依賴的 Android Gradle 外掛 id 來配置工程的型別,但是我們的元件既可以單獨除錯又可以被其他模組依賴,所以這裡的外掛 id 我們不應該寫死,而是通過在 module 中新增一個 gradle.properties 配置檔案,在配置檔案中新增一個布林型別的變數 isRunAlone,在 build.gradle 中通過 isRunAlone 的值來使用不同的外掛從而配置不同的工程型別,在單獨除錯和整合除錯時直接修改 isRunAlone 的值即可。例如,在 Share 分享元件中的配置:

Android 元件化最佳實踐
Android 元件化最佳實踐

2. 如何動態配置元件的 ApplicationId 和 AndroidManifest 檔案

除了通過依賴的外掛來配置不同的工程,我們還要根據 isRunAlone 的值來修改其他配置,一個 APP 是隻有一個 ApplicationId 的,所以在單獨除錯和整合除錯時元件的 ApplicationId 應該是不同的;一般來說一個 APP 也應該只有一個啟動頁, 在元件單獨除錯時也是需要有一個啟動頁,在整合除錯時如果不處理啟動頁的問題,主工程和元件的 AndroidManifes 檔案合併後就會出現兩個啟動頁,這個問題也是需要解決的。

ApplicationId 和 AndroidManifest 檔案都是可以在 build.gradle 檔案中進行配置的,所以我們同樣通過動態配置元件工程型別時定義的 isRunAlone 這個變數的值來動態修改 ApplicationId 和 AndroidManifest。首先我們要新建一個 AndroidManifest.xml 檔案,加上原有的 AndroidManifest 檔案,在兩個檔案中就可以分別配置單獨除錯和整合除錯時的不同的配置,如圖:

Android 元件化最佳實踐

其中 AndroidManifest 檔案中的內容如下:

// main/manifest/AndroidManifest.xml 單獨除錯
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
    package="com.loong.share">

    <application
        android:allowBackup="true"
        android:icon="@mipmap/ic_launcher"
        android:label="@string/app_name"
        android:roundIcon="@mipmap/ic_launcher_round"
        android:supportsRtl="true"
        android:theme="@style/AppTheme">
        <activity android:name=".ShareActivity">
            <intent-filter>
                <action android:name="android.intent.action.MAIN" />

                <category android:name="android.intent.category.LAUNCHER" />
            </intent-filter>
        </activity>
    </application>

</manifest>

// main/AndroidManifest.xml 整合除錯
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
    package="com.loong.share">

    <application android:theme="@style/AppTheme">
        <activity android:name=".ShareActivity"/>
    </application>

</manifest>
複製程式碼

然後在 build.gradle 中通過判斷 isRunAlone 的值,來配置不同的 ApplicationId 和 AndroidManifest.xml 檔案的路徑:

// share 元件的 build.gradle

android {
    defaultConfig {
        if (isRunAlone.toBoolean()) {
            // 單獨除錯時新增 applicationId ,整合除錯時移除
            applicationId "com.loong.login"
        }
        ...
    }
    
    sourceSets {
        main {
            // 單獨除錯與整合除錯時使用不同的 AndroidManifest.xml 檔案
            if (isRunAlone.toBoolean()) {
                manifest.srcFile `src/main/manifest/AndroidManifest.xml`
            } else {
                manifest.srcFile `src/main/AndroidManifest.xml`
            }
        }
    }
}

複製程式碼

到這裡我們就解決了元件化開發時遇到的第一個問題,實現了元件的單獨除錯與整合除錯,並在不同情況時使用的不同配置。當然 build.gradle 中通過 Android Gradle 外掛,我們還可以根據不同工程配置不同的 Java 原始碼、不同的 resource 資原始檔等的,有了上面問題的解決方式,這些問題就都可以解決了。

四、元件間資料傳遞與方法的相互呼叫

由於主專案與元件,元件與元件之間都是不可以直接使用類的相互引用來進行資料傳遞的,那麼在開發過程中如果有元件間的資料傳遞時應該如何解決呢,這裡我們可以採用 [介面 + 實現] 的方式來解決。

在這裡可以新增一個 ComponentBase 模組,這個模組被所有的元件依賴,在這個模組中分別新增定義了元件可以對外提供訪問自身資料的抽象方法的 Service。ComponentBase 中還提供了一個 ServiceFactory,每個元件中都要提供一個類實現自己對應的 Service 中的抽象方法。在元件載入後,需要建立一個實現類的物件,然後將實現了 Service 的類的物件新增到 ServiceFactory 中。這樣在不同元件互動時就可以通過 ServiceFactory 獲取想要呼叫的元件的介面實現,然後呼叫其中的特定方法就可以實現元件間的資料傳遞與方法呼叫。

當然,ServiceFactory 中也會提供所有的 Service 的空實現,在元件單獨除錯或部分整合除錯時避免出現由於實現類物件為空引起的空指標異常。

下面我們就按照這個方法來解決元件間資料傳遞與方法的相互呼叫這個問題,這裡我們通過分享元件 中呼叫 登入元件 中的方法來獲取登入狀態這個場景來演示。

1. 建立 componentbase 模組

AndroidStudio 中建立模組比較簡單,通過選單欄中的 File -> New -> New Module 來建立我們的 componentbase 模組。需要注意的是我們在建立元件時需要使用 Phone & Tablet Module ,建立 componentbase 模組時使用 Android Library 來建立,其中的區別是通過 Phone & Tablet Module 建立的預設是 APP 工程,通過 Android Library 建立的預設是 Library 工程,區別我們上面已經說過了。當然如果選錯了也不要緊,在 buidl.gradle 中也可以自己來修改配置。如下圖:

Android 元件化最佳實踐

這裡 Login 元件中提供獲取登入狀態和獲取登入使用者 AccountId 的兩個方法,分享元件中的分享操作需要使用者登入才可以進行,如果使用者未登入則不進行分享操作。我們先看一下 componentbase 模組中的檔案結構:

Android 元件化最佳實踐

其中 service 資料夾中定義介面, IAccountService 介面中定義了 Login 元件向外提供的資料傳遞的介面方法,empty_service 中是 service 中定義的介面的空實現,ServiceFactory 接收元件中實現的介面物件的註冊以及向外提供特定元件的介面實現。

// IAccountService
public interface IAccountService {

    /**
     * 是否已經登入
     * @return
     */
    boolean isLogin();

    /**
     * 獲取登入使用者的 AccountId
     * @return
     */
    String getAccountId();
}

// EmptyAccountService
public class EmptyAccountService implements IAccountService {
    @Override
    public boolean isLogin() {
        return false;
    }

    @Override
    public String getAccountId() {
        return null;
    }
}

// ServiceFacoty
public class ServiceFactory {

    private IAccountService accountService;

    /**
     * 禁止外部建立 ServiceFactory 物件
     */
    private ServiceFactory() {
    }

    /**
     * 通過靜態內部類方式實現 ServiceFactory 的單例
     */
    public static ServiceFactory getInstance() {
        return Inner.serviceFactory;
    }

    private static class Inner {
        private static ServiceFactory serviceFactory = new ServiceFactory();
    }

    /**
     * 接收 Login 元件實現的 Service 例項
     */
    public void setAccountService(IAccountService accountService) {
        this.accountService = accountService;
    }

    /**
     * 返回 Login 元件的 Service 例項
     */
    public IAccountService getAccountService() {
        if (accountService == null) {
            accountService = new EmptyAccountService();
        }
        return accountService;
    }
}
複製程式碼

前面我們提到的元件化架構圖中,所有的元件都依賴 Base 模組,而 componentbase 模組也是所有元件需要依賴的,所以我們可以讓 Base 模組依賴 componentbase 模組,這樣在元件中依賴 Base 模組後就可以訪問 componentbase 模組中的類。

2. Login 元件在 ServiceFactory 中註冊介面物件

在 componentbase 定義好 Login 元件需要提供的 Service 後,Login 元件需要依賴 componentbase 模組,然後在 Login 元件中建立類實現 IAccountService 介面並實現其中的介面方法,並在 Login 元件初始化(最好是在 Application 中) 時將 IAccountService 介面的實現類物件註冊到 ServiceFactory 中。相關程式碼如下:

// Base 模組的 build.gradle
dependencies {
    api project (`:componentbase`)
    ...
}

// login 元件的 build.gradle
dependencies {
    implementation fileTree(dir: `libs`, include: [`*.jar`])
    implementation project (`:base`)
}

// login 元件中的 IAccountService 實現類
public class AccountService implements IAccountService {
    @Override
    public boolean isLogin() {
        return AccountUtils.userInfo != null;
    }

    @Override
    public String getAccountId() {
        return AccountUtils.userInfo == null ? null : AccountUtils.userInfo.getAccountId();
    }
}

// login 元件中的 Aplication 類
public class LoginApp extends BaseApp {

    @Override
    public void onCreate() {
        super.onCreate();
        // 將 AccountService 類的例項註冊到 ServiceFactory
        ServiceFactory.getInstance().setAccountService(new AccountService());
    }
}
複製程式碼

以上程式碼就是 Login 元件中對外提供服務的關鍵程式碼,到這裡有的小夥伴可能想到了,一個專案時只能有一個 Application 的,Login 作為元件時,主模組的 Application 類會初始化,而 Login 元件中的 Applicaiton 不會初始化。確實是存在這個問題的,我們這裡先將 Service 的註冊放到這裡,稍後我們會解決 Login 作為元件時 Appliaciton 不會初始化的問題。

3. Share 元件與 Login 元件實現資料傳遞

Login 元件中將 IAccountService 的實現類物件註冊到 ServiceFactory 中以後,其他模組就可以使用這個 Service 與 Login 元件進行資料傳遞,我們在 Share 元件中需要使用登入狀態,接下來我們看 Share 元件中如何使用 Login 元件提供的 Service。

同樣,Share 元件也是依賴了 Base 模組的,所以也可以直接訪問到 componentbase 模組中的類,在 Share 元件中直接通過 ServiceFactory 物件的 getAccountService 即可獲取到 Login 元件提供的 IAccountService 介面的實現類物件,然後通過呼叫該物件的方法即可實現與 Login 元件的資料傳遞。主要程式碼如下:

// Share 元件的 buidl.gradle
dependencies {
    implementation project (`:base`)
    ...
}

// Share 元件的 ShareActivity
public class ShareActivity extends AppCompatActivity {

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_share);

        share();
    }

    private void share() {
        if(ServiceFactory.getInstance().getAccountService().isLogin()) {
            Toast.makeText(this, "分享成功", Toast.LENGTH_SHORT);
        } else {
            Toast.makeText(this, "分享失敗:使用者未登入", Toast.LENGTH_SHORT);
        }
    }
}
複製程式碼

這樣的開發模式實現了各個元件間的資料傳遞都是基於介面程式設計,介面和實現完全分離,所以就實現了元件間的解耦。在元件內部的實現類對方法的實現進行修改時,更極端的情況下,我們直接刪除、替換了元件時,只要新加的元件實現了對應 Service 中的抽象方法並在初始化時將實現類物件註冊到 ServiceFactory 中,其他與這個元件有資料傳遞的元件都不需要有任何修改。

到這裡我們元件間資料傳遞和方法呼叫的問題就已經解決了,其實,元件間互動還有很多其他的方式,比如 EventBus,廣播,資料持久化等方式,但是往往這些方式的互動會不那麼直觀,所以對通過 Service 這種形式可以實現的互動,我們最好通過這種方式進行。

4. 元件 Application 的動態配置

上面提到了由於 Application 的替換原則,在主模組中有 Application 等情況下,元件在集中除錯時其 Applicaiton 不會初始化的問題。而我們元件的 Service 在 ServiceFactory 的註冊又必須放到元件初始化的地方。

為了解決這個問題可以將元件的 Service 類強引用到主 Module 的 Application 中進行初始化,這就必須要求主模組可以直接訪問元件中的類。而我們又不想在開發過程中主模組能訪問元件中的類,這裡可以通過反射來實現元件 Application 的初始化。

1)第一步:在 Base 模組中定義抽象類 BaseApp 繼承 Application,裡面定義了兩個方法,initModeApp 是初始化當前元件時需要呼叫的方法,initModuleData 是所有元件的都初始化後再呼叫的方法。

// Base 模組中定義
public abstract class BaseApp extends Application {
    /**
     * Application 初始化
     */
    public abstract void initModuleApp(Application application);

    /**
     * 所有 Application 初始化後的自定義操作
     */
    public abstract void initModuleData(Application application);
}
複製程式碼

2)第二步:所有的元件的 Application 都繼承 BaseApp,並在對應的方法中實現操作,我們這裡還是以 Login 元件為例,其 LoginApp 實現了 BaseApp 介面,其 initModuleApp 方法中完成了在 ServiceFactory 中註冊自己的 Service 物件。在單獨除錯時 onCreate() 方法中也會呼叫 initModuleApp() 方法完成在 ServiceFactory 中的註冊操作。

// Login 元件的 LoginApp
public class LoginApp extends BaseApp {

    @Override
    public void onCreate() {
        super.onCreate();
        initModuleApp(this);
        initModuleData(this);
    }

    @Override
    public void initModuleApp(Application application) {
        ServiceFactory.getInstance().setAccountService(new AccountService());
    }

    @Override
    public void initModuleData(Application application) {

    }
}
複製程式碼

3)第三步:在 Base 模組中定義 AppConfig 類,其中的 moduleApps 是一個靜態的 String 陣列,我們將需要初始化的元件的 Application 的完整類名放入到這個陣列中。

// Base 模組的 AppConfig
public class AppConfig {
    private static final String LoginApp = "com.loong.login.LoginApp";

    public static String[] moduleApps = {
            LoginApp
    };
}
複製程式碼

4)第四步:主 module 的 Application 也繼承 BaseApp ,並實現兩個初始化方法,在這兩個初始化方法中遍歷 AppcConfig 類中定義的 moduleApps 陣列中的類名,通過反射,初始化各個元件的 Application。

// 主 Module 的 Applicaiton
public class MainApplication extends BaseApp {
    @Override
    public void onCreate() {
        super.onCreate();
        
        // 初始化元件 Application
        initModuleApp(this);
        
        // 其他操作
        
        // 所有 Application 初始化後的操作
        initModuleData(this);
        
    }

    @Override
    public void initModuleApp(Application application) {
        for (String moduleApp : AppConfig.moduleApps) {
            try {
                Class clazz = Class.forName(moduleApp);
                BaseApp baseApp = (BaseApp) clazz.newInstance();
                baseApp.initModuleApp(this);
            } catch (ClassNotFoundException e) {
                e.printStackTrace();
            } catch (IllegalAccessException e) {
                e.printStackTrace();
            } catch (InstantiationException e) {
                e.printStackTrace();
            }
        }
    }

    @Override
    public void initModuleData(Application application) {
        for (String moduleApp : AppConfig.moduleApps) {
            try {
                Class clazz = Class.forName(moduleApp);
                BaseApp baseApp = (BaseApp) clazz.newInstance();
                baseApp.initModuleData(this);
            } catch (ClassNotFoundException e) {
                e.printStackTrace();
            } catch (IllegalAccessException e) {
                e.printStackTrace();
            } catch (InstantiationException e) {
                e.printStackTrace();
            }
        }
    }
}
複製程式碼

到這裡我們就通過反射,完成了元件 Application 的初始化操作,也實現了元件與化中的解耦需求。

四、元件間介面跳轉

Android 中的介面跳轉,主要有顯式 Intent 和隱式 Intent 兩種。在同一個元件中,因為類可以自由訪問,所以介面跳轉可以通過顯式 Intent 的方式實現。而在元件化開發中,由於不同元件式沒有相互依賴的,所以不可以直接訪問彼此的類,這時候就沒辦法通過顯式的方式實現了。

Android 中提供的隱式 Intent 的方式可以實現這個需求,但是隱式 Intent 需要通過 AndroidManifest 集中管理,協作開發比較麻煩。所以在這裡我們採取更加靈活的一種方式,使用 Alibaba 開源的 ARouter 來實現。

一個用於幫助 Android App 進行元件化改造的框架 —— 支援模組間的路由、通訊、解耦

由 github 上 ARouter 的介紹可以知道,它可以實現元件間的路由功能。路由是指從一個介面上收到資料包,根據資料路由包的目的地址進行定向並轉發到另一個介面的過程。這裡可以體現出路由跳轉的特點,非常適合元件化解耦。

要使用 ARouter 進行介面跳轉,需要我們的元件對 Arouter 新增依賴,因為所有的元件都依賴了 Base 模組,所以我們在 Base 模組中新增 ARouter 的依賴即可。其它元件共同依賴的庫也最好都放到 Base 中統一依賴。

這裡需要注意的是,arouter-compiler 的依賴需要所有使用到 ARouter 的模組和元件中都單獨新增,不然無法在 apt 中生成索引檔案,也就無法跳轉成功。並且在每一個使用到 ARouter 的模組和元件的 build.gradle 檔案中,其 android{} 中的 javaCompileOptions 中也需要新增特定配置。

// Base 模組的 build.gradle
dependencies {
    api `com.alibaba:arouter-api:1.3.1`
    // arouter-compiler 的註解依賴需要所有使用 ARouter 的 module 都新增依賴
    annotationProcessor `com.alibaba:arouter-compiler:1.1.4`
}
複製程式碼
// 所有使用到 ARouter 的元件和模組的 build.gradle
android {
    defaultConfig {
        ...
        javaCompileOptions {
            annotationProcessorOptions {
                arguments = [ moduleName : project.getName() ]
            }
        }
    }
}

dependencies {
    ...
    implementation project (`:base`)
    annotationProcessor `com.alibaba:arouter-compiler:1.1.4`
}
複製程式碼
// 主專案的 build.gradle 需要新增對 login 元件和 share 元件的依賴
dependencies {
    // ... 其他
    implementation project(`:login`)
    implementation project(`:share`)
}
複製程式碼

新增了對 ARouter 的依賴後,還需要在專案的 Application 中將 ARouter 初始化,我們這裡將 ARouter 的初始化工作放到主專案 Application 的 onCreate 方法中,在應用啟動的同時將 ARouter 初始化。

// 主專案的 Application
public class MainApplication extends Application {
    @Override
    public void onCreate() {
        super.onCreate();

        // 初始化 ARouter
        if (isDebug()) {           
            // 這兩行必須寫在init之前,否則這些配置在init過程中將無效
            
            // 列印日誌
            ARouter.openLog();     
            // 開啟除錯模式(如果在InstantRun模式下執行,必須開啟除錯模式!線上版本需要關閉,否則有安全風險)
            ARouter.openDebug();   
        }
        
        // 初始化 ARouter
        ARouter.init(this);
        
        // 其他操作 ...
    }

    private boolean isDebug() {
        return BuildConfig.DEBUG;
    }
    
    // 其他程式碼 ...
}
複製程式碼

這裡我們以主專案跳登入介面,然後登入介面登入成功後跳分享元件的分享介面為例。其中分享功能還使用了我們上面提到的呼叫登入元件的 Service 對登入狀態進行判斷。

首先,需要在登入和分享元件中分別新增 LoginActivity 和 ShareActivity ,然後分別為兩個 Activity 新增註解 Route,其中 path 是跳轉的路徑,這裡的路徑需要注意的是至少需要有兩級,/xx/xx

Login 元件的 LoginActivity:

@Route(path = "/account/login")
public class LoginActivity extends AppCompatActivity {

    private TextView tvState;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_login);

        initView();
        updateLoginState();
    }

    private void initView() {
        tvState = (TextView) findViewById(R.id.tv_login_state);
    }

    public void login(View view) {
        AccountUtils.userInfo = new UserInfo("10086", "Admin");
        updateLoginState();
    }

    private void updateLoginState() {
        tvState.setText("這裡是登入介面:" + (AccountUtils.userInfo == null ? "未登入" : AccountUtils.userInfo.getUserName()));
    }

    public void exit(View view) {
        AccountUtils.userInfo = null;
        updateLoginState();
    }

    public void loginShare(View view) {
        ARouter.getInstance().build("/share/share").withString("share_content", "分享資料到微博").navigation();
    }
}

複製程式碼

Share 元件的 ShareActivity:

@Route(path = "/share/share")
public class ShareActivity extends AppCompatActivity {
    private TextView tvState;
    private Button btnLogin, btnExit;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_login);

        initView();
        updateLoginState();
    }

    private void initView() {
        tvState = (TextView) findViewById(R.id.tv_login_state);
    }

    public void login(View view) {
        AccountUtils.userInfo = new UserInfo("10086", "Admin");
        updateLoginState();
    }

    public void exit(View view) {
        AccountUtils.userInfo = null;
        updateLoginState();
    }

    public void loginShare(View view) {
        ARouter.getInstance().build("/share/share").withString("share_content", "分享資料到微博").navigation();
    }
    
    private void updateLoginState() {
        tvState.setText("這裡是登入介面:" + (AccountUtils.userInfo == null ? "未登入" : AccountUtils.userInfo.getUserName()));
    }
}
複製程式碼

然後在 MainActivity 中通過 ARouter 跳轉,其中build 處填的是 path 地址,withXXX 處填的是 Activity 跳轉時攜帶的引數的 key 和 value,navigation 就是發射了路由跳轉。

// 主專案的 MainActivity
public class MainActivity extends AppCompatActivity {

    @Override
    protected void onCreate(@Nullable Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
    }

    /**
     * 跳登入介面
     * @param view
     */
    public void login(View view){
        ARouter.getInstance().build("/account/login").navigation();
    }

    /**
     * 跳分享介面
     * @param view
     */
    public void share(View view){
        ARouter.getInstance().build("/share/share").withString("share_content", "分享資料到微博").navigation();
    }
}
複製程式碼

如果研究過 ARouter 原始碼的同學可能知道,ARouter擁有自身的編譯時註解框架,其跳轉功能是通過編譯時生成的輔助類完成的,最終的實現實際上還是呼叫了 startActivity。

路由的另外一個重要作用就是過濾攔截,以 ARouter 為例,如果我們定義了過濾器,在模組跳轉前會遍歷所有的過濾器,然後通過判斷跳轉路徑來找到需要攔截的跳轉,比如上面我們提到的分享功能一般都是需要使用者登入的,如果我們不想在所有分享的地方都新增登入狀態的判斷,我們就可以使用路由的過濾功能,我們就以這個功能來演示,我們可以定義一個簡單的過濾器:

// Login 模組中的登入狀態過濾攔截器
@Interceptor(priority = 8, name = "登入狀態攔截器")
public class LoginInterceptor implements IInterceptor {

    private Context context;

    @Override
    public void process(Postcard postcard, InterceptorCallback callback) {

        // onContinue 和 onInterrupt 至少需要呼叫其中一種,否則不會繼續路由
        
        if (postcard.getPath().equals("/share/share")) {
            if (ServiceFactory.getInstance().getAccountService().isLogin()) {
                callback.onContinue(postcard);  // 處理完成,交還控制權
            } else {
                callback.onInterrupt(new RuntimeException("請登入")); // 中斷路由流程
            }
        } else {
            callback.onContinue(postcard);  // 處理完成,交還控制權
        }

    }

    @Override
    public void init(Context context) {
        // 攔截器的初始化,會在sdk初始化的時候呼叫該方法,僅會呼叫一次
        this.context = context;
    }
}
複製程式碼

自定義的過濾器需要通過 @Tnterceptor 來註解,priority 是優先順序,name 是對這個攔截器的描述。以上程式碼中通過 Postcard 獲取跳轉的 path,然後通過 path 以及特定的需求來判斷是否攔截,在這裡是通過對登入狀態的判斷進行攔截,如果已經登入就繼續跳轉,如果未登入就攔截跳轉。

五、主專案如何在不直接訪問元件中具體類的情況下使用元件的 Fragment

除了 Activity 的跳轉,我們在開發過程中也會經常使用 Fragment,一種很常見的樣式就是應用主頁 Activity 中包含了多個隸屬不同元件的 Fragment。一般情況下,我們都是直接通過訪問具體 Fragment 類的方式實現 Fragment 的例項化,但是現在為了實現模組與元件間的解耦,在移除元件時不會由於引用的 Fragment 不存在而編譯失敗,我們就不能模組中直接訪問元件的 Fragment 類。

這個問題我們依舊可以通過反射來解決,通過來初始化 Fragment 物件並返回給 Activity,在 Actiivty 中將 Fragment 新增到特定位置即可。

也可以通過我們的 componentbase 模組來實現這個功能,我們可以把 Fragment 的初始化工作放到每一個元件中,模組需要使用元件的 Fragment 時,通過 componentbase 提供的 Service 中的方法來實現 Fragment 的初始化。

這裡我們通過第二種方式實現在 Login 元件中提供一個 UserFragment 來演示。

首先,在 Login 元件中建立 UserFragment,然後在 IAccountService 介面中新增 newUserFragment 方法返回一個 Fragment,在 Login 元件中的 AccountService 和 componentbase 中 IAccountService 的空實現類中實現這個方法,然後在主模組中通過 ServiceFactory 獲取 IAccountService 的實現類物件,呼叫其 newUserFragment 即可獲取到 UserFragment 的例項。以下是主要程式碼:

// componentbase 模組的 IAccountService 
public interface IAccountService {
    // 其他程式碼 ...

    /**
     * 建立 UserFragment
     * @param activity
     * @param containerId
     * @param manager
     * @param bundle
     * @param tag
     * @return
     */
    Fragment newUserFragment(Activity activity, int containerId, FragmentManager manager, Bundle bundle, String tag);
}

// Login 元件中的 AccountService
public class AccountService implements IAccountService {
    // 其他程式碼 ...

    @Override
    public Fragment newUserFragment(Activity activity, int containerId, FragmentManager manager, Bundle bundle, String tag) {
        FragmentTransaction transaction = manager.beginTransaction();
        // 建立 UserFragment 例項,並新增到 Activity 中
        Fragment userFragment = new UserFragment();
        transaction.add(containerId, userFragment, tag);
        transaction.commit();
        return userFragment;
    }
}

// 主模組的 FragmentActivity
public class FragmentActivity extends AppCompatActivity {

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_fragment);
        
        // 通過元件提供的 Service 實現 Fragment 的例項化
        ServiceFactory.getInstance().getAccountService().newUserFragment(this, R.id.layout_fragment, getSupportFragmentManager(), null, "");
    }
}
複製程式碼

這樣就實現了 Fragment 的例項化,滿足瞭解耦的要求,並保證了業務分離是不會造成編譯失敗及 App 崩潰。

六、元件整合除錯

上面解決的幾個問題主要是元件開發過程中必須要解決的問題,當元件開發完成後我們可能需要將特定幾個元件整合除錯,而不是將所有的元件全部整合進行除錯。這時候我們要滿足只整合部分元件時可以編譯通過,不會因為未整合某些元件而出現編譯失敗的問題。

其實這個問題我們在解決上面幾個問題的時候就已經解決了。不管是元件間還是模組與元件間都沒有直接使用其中的類進行操作,而是通過 componentbase 模組中的 Service 來實現的,而 componentbase 模組中所有 Service 介面的空實現也保證了即使特定元件沒有初始化,在其他元件呼叫其對應方法時也不會出現異常。這種面向介面程式設計的方式,滿足了我們不管是元件間還是模組與元件間的相互解耦。

這時候元件化的架構圖就成了這樣:

Android 元件化最佳實踐

七、元件解耦的目標及程式碼隔離

解耦目標

程式碼解耦的首要目標就是元件之間的完全隔離,在開發過程中我們要時刻牢記,我們不僅不能直接使用其他元件中的類,最好能根本不瞭解其中的實現細節。

程式碼隔離

通過以上幾個問題的解決方式可以看到,我們在極力的避免元件間及模組與元件間類的直接引用。不過即使通過 componentbase 中提供 Service 的方式解決了直接引用類的問題,但是我們在主專案通過 implementation 新增對 login 和 share 元件的依賴後,在主專案中依舊是可以訪問到 login 和 share 元件中的類的。

這種情況下即使我們的目標是面向介面程式設計,但是隻要能直接訪問到元件中的類,就存在有意或無意的直接通過訪問類的方式使用到元件中的程式碼的可能,如果真的出現了這種情況,我們上面說的解耦就會完全白做了。

我們希望的元件依賴是隻有在打包過程中才能直接引用元件中的類,在開發階段,所有元件中的類我們都是不可以訪問的。只有實現了這個目標,才能從根本上杜絕直接引用元件中類的問題。

這個問題我們可以通過 Gradle 提供的方式來解決,Gradle 3.0 提供了新的依賴方式 runtimeOnly ,通過 runtimeOnly 方式依賴時,依賴項僅在執行時對模組及其消費者可用,編譯期間依賴項的程式碼對其消費者時完全隔離的。

所以我們將主專案中對 Login 元件和 Share 元件的依賴方式修改為 runtimeOnly 的方式就可以解決開發階段可以直接引用到元件中類的問題。

// 主專案的 build.gradle
dependencies {
    // 其他依賴 ...
    runtimeOnly project(`:login`)
    runtimeOnly project(`:share`)
}
複製程式碼

解決了程式碼隔離的問題,另一個問題就會又浮現出來。元件開發中不僅要實現程式碼的隔離,還要實現資原始檔的隔離。解決程式碼隔離的 runtimeOnly 並不能做到資源隔離。通過 runtimeOnly 依賴元件後,在主專案中還是可以直接使用到元件中的資原始檔。

為了解決這個問題,我們可以在每個元件的 build.gradle 中新增 resourcePrefix 配置來固定這個元件中的資源字首。不過 resourcePrefix 配置只能限定 res 中 xml 檔案中定義的資源,並不能限定圖片資源,所以我們在往元件中新增圖片資源時要手動限制資源字首。並將多個元件中都會用到的資源放入 Base 模組中。這樣我們就可以在最大限度上實現元件間資源的隔離。

如果元件配置了 resourcePrefix ,其 xml 中定義的資源沒有以 resourcePrefix 的值作為字首,在對應的 xml 中定義的資源會報紅。resourcePrefix 的值就是指定的元件中 xml 資源的字首。以 Login 元件為例:

// Login 元件的 build.gradle
android {
    resourcePrefix "login_"
    // 其他配置 ...
}
複製程式碼

Login 元件中新增 resourcePrefix 配置後,我們會發現 res 中 xml 定義的資源都報紅:

Android 元件化最佳實踐

而我們修改字首後則報紅消失,顯示恢復正常:

Android 元件化最佳實踐

到這裡解決了元件間程式碼及資源隔離的問題也就解決了。

八、總結

解決了上面提到的六個問題,元件化開發中遇到的主要問題也就全部解決了。其中最關鍵的就是模組與元件間的解耦。在設計之初也參考了目前主流的幾種元件化方案,後來從使用難度、理解難度、維護難度、擴充套件難度等方面考慮,最終確定了目前的元件化方案。

Demo地址 :https://github.com/renxuelong/ComponentDemo

Android 元件化最佳實踐
歡迎關注我的微信公眾號「玉剛說」,接收第一手技術乾貨

相關文章