Android 組件化最佳實踐

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

原做者:任雪龍android

版權聲明:本文版權歸微信公衆號玉剛說全部,未經許可,不得以任何形式轉載git

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

演示爲先

在項目的開發過程當中,隨着開發人員的增多及功能的增長,若是提早沒有使用合理的開發架構,那麼代碼會越來臃腫,功能間代碼耦合也會愈來愈嚴重,這時候爲了保證項目代碼的質量,咱們就必須進行重構。編程

比較簡單的開發架構是按照功能模塊進行拆分,也就是用 Android 開發中的 module 這個概念,每一個功能都是一個 module,每一個功能的代碼都在本身所屬的 module 中添加。這樣的設計在各個功能相互直接比較獨立的狀況下是比較合理的,可是當多個模塊中涉及到相同功能時代碼的耦合又會增長。api

例如首頁模塊和直播間模塊中均可能涉及到了視頻播放的功能,這時候無論將播放控制的代碼放到首頁仍是直播間,開發過程當中都會發現,咱們想要解決的代碼耦合狀況又又又又出現了。爲了進一步解決這個問題,組件化的開發模式順勢而來。數組

1、組件化和模塊化的區別

上面說到了從普通的無架構到模塊化,再由模塊化到組件化,那麼其中的界限是什麼,模塊化和組件化的本質區別又是什麼?爲了解決這些問題,咱們就要先了解 「模塊」 和 「組件」 的區別。安全

模塊

模塊指的是獨立的業務模塊,好比剛纔提到的 [首頁模塊]、[直播間模塊] 等。bash

組件

組件指的是單一的功能組件,如 [視頻組件]、[支付組件] 等,每一個組件均可以以一個單獨的 module 開發,而且能夠單獨抽出來做爲 SDK 對外發布使用。微信

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

組件化基礎架構圖

上面是一個很是基礎的組件化架構圖,圖中從上向下分別爲應用層、組件層和基礎層。

基礎層: 基礎層很容易理解,其中包含的是一些基礎庫以及對基礎庫的封裝,好比經常使用的圖片加載,網絡請求,數據存儲操做等等,其餘模塊或者組件均可以引用同一套基礎庫,這樣不但只須要開發一套代碼,還解耦了基礎功能和業務功能的耦合,在基礎庫變動時更加容易操做。

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

應用層: 組件層往上是應用層,這裏爲了簡單,只添加了一個 APP ,APP 就至關於咱們的模塊,一個具體的業務模塊會按需引用不一樣的組件,最終實現業務功能,這裏若是又多個業務模塊,就能夠各自按需引用組件,最後將各個模塊統籌輸出 APP。

到這裏咱們最簡單的組件化架構就已經可使用了,可是這只是最理想的狀態下的架構,實際的開發中,不一樣的組件不可能完全的相互隔離,組件中確定會有相互傳遞數據、調用方法、頁面跳轉等狀況。

好比直播組件中用戶須要刷禮物,刷禮物就須要支付組件的支持,而支付組件中支付操做是必須須要登陸狀態、用戶 ID 等信息。若是當前未登陸,是須要先跳轉到登陸組件中進行登陸操做,登陸成功後才能正常的進行支付流程。

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

2、組件化開發須要解決的問題

在實現組件化的過程當中,同一個問題可能有不一樣的技術路徑能夠解決,可是須要解決的問題主要有如下幾點:

  1. 每一個組件都是一個完整的總體,因此組件開發過程當中要知足單獨運行及調試的要求,這樣還能夠提高開發過程當中項目的編譯速度。

  2. 數據傳遞與組件間方法的相互調用,這也是上面咱們提到的一個必需要解決的問題。

  3. 組件間界面跳轉,不一樣組件之間不只會有數據的傳遞,也會有相互的頁面跳轉。在組件化開發過程當中如何在不相互依賴的狀況下實現互相跳轉?

  4. 主項目不直接訪問組件中具體類的狀況下,如何獲取組件中 Fragment 的實例並將組件中的 Fragment 實例添加到主項目的界面中?

  5. 組件開發完成後相互之間的集成調試如何實現?還有就是在集成調試階段,依賴多個組件進行開發時,若是實現只依賴部分組件時能夠編譯經過?這樣也會下降編譯時間,提高效率。

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

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

3、組件單獨調試

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 分享組件中的配置:

2. 如何動態配置組件的 ApplicationId 和 AndroidManifest 文件

除了經過依賴的插件來配置不一樣的工程,咱們還要根據 isRunAlone 的值來修改其餘配置,一個 APP 是隻有一個 ApplicationId 的,因此在單獨調試和集成調試時組件的 ApplicationId 應該是不一樣的;通常來講一個 APP 也應該只有一個啓動頁, 在組件單獨調試時也是須要有一個啓動頁,在集成調試時若是不處理啓動頁的問題,主工程和組件的 AndroidManifes 文件合併後就會出現兩個啓動頁,這個問題也是須要解決的。

ApplicationId 和 AndroidManifest 文件都是能夠在 build.gradle 文件中進行配置的,因此咱們一樣經過動態配置組件工程類型時定義的 isRunAlone 這個變量的值來動態修改 ApplicationId 和 AndroidManifest。首先咱們要新建一個 AndroidManifest.xml 文件,加上原有的 AndroidManifest 文件,在兩個文件中就能夠分別配置單獨調試和集成調試時的不一樣的配置,如圖:

其中 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 資源文件等的,有了上面問題的解決方式,這些問題就均可以解決了。

4、組件間數據傳遞與方法的相互調用

因爲主項目與組件,組件與組件之間都是不能夠直接使用類的相互引用來進行數據傳遞的,那麼在開發過程當中若是有組件間的數據傳遞時應該如何解決呢,這裏咱們能夠採用 [接口 + 實現] 的方式來解決。

在這裏能夠添加一個 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 中也能夠本身來修改配置。以下圖:

這裏 Login 組件中提供獲取登陸狀態和獲取登陸用戶 AccountId 的兩個方法,分享組件中的分享操做須要用戶登陸才能夠進行,若是用戶未登陸則不進行分享操做。咱們先看一下 componentbase 模塊中的文件結構:

其中 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 的初始化操做,也實現了組件與化中的解耦需求。

4、組件間界面跳轉

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 以及特定的需求來判斷是否攔截,在這裏是經過對登陸狀態的判斷進行攔截,若是已經登陸就繼續跳轉,若是未登陸就攔截跳轉。

5、主項目如何在不直接訪問組件中具體類的狀況下使用組件的 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 崩潰。

6、組件集成調試

上面解決的幾個問題主要是組件開發過程當中必需要解決的問題,當組件開發完成後咱們可能須要將特定幾個組件集成調試,而不是將全部的組件所有集成進行調試。這時候咱們要知足只集成部分組件時能夠編譯經過,不會由於未集成某些組件而出現編譯失敗的問題。

其實這個問題咱們在解決上面幾個問題的時候就已經解決了。無論是組件間仍是模塊與組件間都沒有直接使用其中的類進行操做,而是經過 componentbase 模塊中的 Service 來實現的,而 componentbase 模塊中全部 Service 接口的空實現也保證了即便特定組件沒有初始化,在其餘組件調用其對應方法時也不會出現異常。這種面向接口編程的方式,知足了咱們無論是組件間仍是模塊與組件間的相互解耦。

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

7、組件解耦的目標及代碼隔離

解耦目標

代碼解耦的首要目標就是組件之間的徹底隔離,在開發過程當中咱們要時刻牢記,咱們不只不能直接使用其餘組件中的類,最好能根本不瞭解其中的實現細節。

代碼隔離

經過以上幾個問題的解決方式能夠看到,咱們在極力的避免組件間及模塊與組件間類的直接引用。不過即便經過 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 定義的資源都報紅:

而咱們修改前綴後則報紅消失,顯示恢復正常:

到這裏解決了組件間代碼及資源隔離的問題也就解決了。

8、總結

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

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

歡迎關注個人微信公衆號「玉剛說」,接收第一手技術乾貨
相關文章
相關標籤/搜索