Android技術棧(二)組件化改造

1.爲何要組件化?

國內都比較流行開發超級APP,也就是我全都要,什麼功能都想加進去,這致使業務邏輯變得愈來愈複雜. java

這時咱們會開始面臨兩個問題:android

  • 首先,咱們的res文件夾下的資源將會迎來爆炸式地增加,而且咱們都知道res文件夾不能分層,它只能按module進行劃分,因此你的layoutmipmap等文件夾將最早被迫害,當這兩個文件夾的資源變多時,你要查找一個layout或者一張圖片都會變得十分費勁
  • 其次,若是此時你的APP仍是隻有一個module,還將會可能致使業務邏輯耦合沒法複用,除非你的編程習慣十分良好,可是絕大多數人都作不到,因此咱們須要用組件化來給本身一些約束,以此創造更高質量的應用程序.

2.使用ARouter對項目進行組件化改造

我特別喜歡ARouter簡介中的一句話:解耦不是前提而是過程.接下來我將介紹如何使用ARouter對項目進行組件化改造git

要組件化,首先你須要建立module來分割你的業務邏輯.要建立新的module能夠在你的project名字上右鍵,而後New->Module github

而後選擇 Android Library便可.
工程中有一個 hostcom.android.applicationmodule,其餘包含業務邏輯的 modulecom.android.library實現, host依賴其餘 module,這就能夠實現組件化中的熱插拔了.

這裏列出我對本身項目裏組件化改造後的目錄結構的摘要編程

dng(project) //項目根
—— host(module) //殼模塊
———— AppGlobal.java //自定義Application類
———— HostActivity.java //用來啓動程序的Activity
—— common(module) //公共模塊
———— PR.java //全部path的常量的集合
———— TTSService.java //從ai模塊下沉的接口
———— Utils.java //通用工具類
—— ai(module) //業務邏輯模塊
———— SpeakerFragment.java //業務邏輯
———— TTSServiceImpl.java //TTSService的具體實現類
—— navi(module) //業務邏輯模塊
———— NaviFragment.java //業務邏輯
———— NaviViewModel.java //業務邏輯
複製代碼

解釋一下:api

先說common模塊,這個模塊須要包含項目中要使用的全部依賴和一些公用的工具類,以後每一個模塊都依賴common模塊,這樣就能夠把common模塊的依賴輕鬆地依賴導入到其餘模塊中去而不用在其餘模塊的build.gradle中重複地寫一大堆腳本.緩存

要想使用ARouter,先要在common模塊的build.gradle中使用api(老版本是compile)引入ARrouter的運行時依賴(下面的版本可能不是最新的,獲取最新版本請到Github獲取最新版本的ARouter)bash

api 'com.alibaba:arouter-api:1.4.1'
複製代碼

相似R文件咱們還能夠在common模塊中定義一個PRjava文件,來保存咱們項目中所用到的全部路由的pathapp

public final class PR
{

    public static final class navi
    {
        public static final String navi = "/navi/navi";
        public static final String location_service = "/navi/location";
    }

    public static final class ai
    {
        public final static String tts_service = "/ai/tts";
        public final static String asr_service = "/ai/asr";
        public final static String speaker = "/ai/speaker";
    }
}
複製代碼

這能夠幫助咱們更好的對頁面按模塊進行分類,同時,其餘模塊導入common模塊時,也會將PR導入進去,但又不須要依賴某個具體實現的模塊,咱們能夠在頁面跳轉時直接引用這些常量,而且集中起來也好統一管理.框架

這裏須要注意一點,在ARouter中是使用path來映射到頁面的,每一個path都必須至少有兩級,而且每一個頁面的第一級不能夠是其餘模塊已經使用過的.

host模塊是,是一個空的APP殼模塊,基本不實現任何業務邏輯,經過在build.gradle中,引用其餘模塊爲本身添加功能.

implementation project(':common')
    implementation project(':navi')
    implementation project(':ai')
複製代碼

AppGlobal是我自定義的Application,咱們須要在這裏面給ARouter進行初始化.注意循序不要錯,不然你可能會看不到一些log,並且在Debug模式下必定要openDebug,不然ARouter只會在第一次運行的時候掃描Dex加載路由表.

public final class AppGlobal extends MultiDexApplication
{
    @Override
    public void onCreate()
    {
        super.onCreate();
        if (BuildConfig.DEBUG)
        {
            ARouter.openLog();     // Print log
            ARouter.openDebug();
        }
        ARouter.init(this);
    }
}
複製代碼

個人HostActivity中差很少就只有這些代碼,能夠看到我獲取了ARouter的單例,而後使用build引用PR傳入path,最後調用navigation獲取其餘模塊的Fragment用來添加到當前Activity中.

Fragment fragment = (Fragment) ARouter.getInstance()
                .build(PR.navi.navi)
                .navigation();

        getSupportFragmentManager()
                .beginTransaction()
                .add(R.id.fragment_container, fragment, PR.ux.desktop)
                .setTransition(FragmentTransaction.TRANSIT_FRAGMENT_OPEN)
                .commit();
複製代碼

而後是navi模塊,由於這個模塊使用了ARouter的註解,記得要先在build.gradle配置ARouter註解處理器的環境(host模塊若是也使用了那麼也要配置)

android {

    //省略...
    
    //ARouter註解處理器啓動參數
    javaCompileOptions {
        annotationProcessorOptions {
            arguments = [AROUTER_MODULE_NAME: project.getName()]
        }
    }
    
}

dependencies {
    //省略..

    //導入公共依賴
    implementation project(':common')
    //聲明ARouter註解處理器
    annotationProcessor 'com.alibaba:arouter-compiler:1.2.2'
}
複製代碼

咱們在navi模塊中使用@Route註解將PR.navi.navi映射到具體的Fragment或者Activity

這樣:

@Route(path = PR.navi.navi)
public class NaviFragment extends Fragment
複製代碼

或者這樣:

@Route(path = PR.navi.navi)
public class NaviActivity extends AppCompatActivity
複製代碼

ARouter這種使用path解耦的方式容許咱們在開發的過程當中更換PR.navi.navi映射到的FragmentActivity,而在代碼修改時把對調用方的影響下降到最小.

但值得注意的是,ARouter對不一樣類型的處理是不同的,若是path指向的是Fragment,你須要獲取navigation的返回值並手動把它添加到FragmentManager中.(若是不瞭解Fragment的同窗能夠看這篇文章 從Activity遷移到Fragment)

Fragment fragment = (Fragment) ARouter.getInstance()
                .build(PR.navi.navi)
                .navigation();
複製代碼

Activity則不須要,它會當即顯示

ARouter.getInstance()
                .build(PR.navi.navi)
                //還能夠設置參數,ARouter會幫你存在Bundle中
                .withString("pathId",UUID.randomUUID().toString())
                //Activity 或 Context
                .navigation(this);
複製代碼

navi模塊是典型的業務邏輯模塊,這裏你可導入一些只有這個模塊纔會使用的專屬第三方SDK,好比我在navi模塊中使用了高德地圖SDK,其餘模塊只須要我這個模塊的地圖功能,但它不該該知道我到底使用的是高德仍是百度仍是騰訊地圖,這就提升了封裝性,在將來改變此模塊的具體實現時,代價也會小得多.

3.自定義全局攔截器、全局降級策略、全局重定向

ARouter實現了module間的路由操做,同時也實現了攔截器的功能,攔截器是一種AOP(面向切面編程),比較經典的使用場景就是處理頁面登陸與否的問題.攔截器會在跳轉之間執行,多個攔截器會按優先級順序依次執行.經過實現IInterceptor接口並標註@Interceptor註解,這樣一來,這個攔截器就被註冊到ARouter當中了.

process方法會傳入PostcardInterceptorCallback,Postcard攜帶這次路由的關鍵信息,而InterceptorCallback則用於處理這次攔截,調用onContinue則放行,又或者調用onInterrupt拋出自定義異常.

攔截器會在ARouter初始化的時候進行異步(不在主線程)初始化,若是第一次路由發生時,還有攔截器沒有初始化完畢,那麼ARouter會等待該攔截器初始化完畢才進行路由.

@Interceptor(priority = 8)
public class TestInterceptor implements IInterceptor {
    @Override
    public void process(Postcard postcard, InterceptorCallback callback) {

    callback.onContinue(postcard);  // 處理完成,交還控制權
        // callback.onInterrupt(new RuntimeException("我以爲有點異常"));
        // 以爲有問題,中斷路由流程
        // 以上兩種至少須要調用其中一種,不然不會繼續路由
    }

    @Override
    public void init(Context context) {
        // 攔截器的初始化,會在ARouter初始化的時候調用該方法,僅會調用一次
    }
}
複製代碼

當頁面未找到時,咱們能夠定義一種降級策略來讓程序繼續運行,此時咱們須要實現DegradeService接口,並用@Route(必須)標註,而後它會在全局範圍內生效,你能夠在onLost回調中自定義降級邏輯.

@Route(path = "/xxx/xxx")
public class DegradeServiceImpl implements DegradeService {
    @Override
    public void onLost(Context context, Postcard postcard) {
        // do something.
    }

    @Override
    public void init(Context context) {

    }
}
複製代碼

有時候頁面咱們須要將path其重定向別的path,這時咱們能夠實現PathReplaceService接口,並用@Route(必須)標註,而後它會在全局範圍內生效.因此若沒有重定向需求記得返回原path

@Route(path = "/xxx/xxx")
public class PathReplaceServiceImpl implements PathReplaceService {
    String forString(String path) {
        return path;    // 按照必定的規則處理以後返回處理後的結果
    }
    Uri forUri(Uri uri) {
        return url;    // 按照必定的規則處理以後返回處理後的結果
    }
    
    @Override
    public void init(Context context) {

    }
}
複製代碼

以上上三種接口中的init方法,只有攔截器的調用時間是特殊的,其餘兩種,都是在第一次使用時纔會進行初始化.

4.接口下沉->暴露服務

有的時候咱們可能須要的不是另一個模塊的頁面,而是它提供的服務(MVC中的Model層),這時這時咱們須要爲本身想要的服務編寫一個接口,並讓他實現IProvider接口,而後把它放到common模塊中, 可是接口的實現依然放在非common的具體的模塊中,好比common模塊的TTSServiceai模塊的TTSServiceImpl.

這種作法被稱爲接口下沉,其實它並非嚴格符合解耦思想的,可是它很是有用,就像你使用了ARouter,但沒人規定你就不能用startActivity了同樣,框架最終的目的仍是爲了方便咱們編碼的,而不是爲了給咱們添堵,更況且最終結果各模塊依然是鬆散耦合的.

服務的初始化時機也是在第一次使用的時候.咱們在common模塊中聲明TTSService接口:

public interface TTSService extends IProvider
{
    void send(String text);

    void stop();
}

複製代碼

並在ai模塊中實現它並使用@Route註解標註

@Route(path = PR.ai.tts_service)
public class TTSServiceImpl implements TTSService
{
    //省略...
}
複製代碼

這樣咱們就能在其餘模塊使用該服務了

TTSService ttsService = (TTSService) ARouter.getInstance()
                .build(PR.ai.tts_service)
                .navigation()
複製代碼

5.ContentProvider->模塊內的Application

有些第三方SDK初始化是必需要在ApplicationonCreate中進行初始化的,可是若是咱們編寫獨立於hostmodule時,要怎麼初始化它們呢?

ARouter並無提供官方的解決方案,可是通過個人實踐,咱們能夠經過聲明ContentProvider並在模塊內AndroidManifest中註冊它來實現初始化功能.

//java
public class ModuleLoader extends ContentProvider
{
    @Override
    public boolean onCreate()
    {
        Context context = getContext();
        //TODO
        return true;
    }
    
    //......

}

//AndroidManifest
<provider
    android:authorities="${applicationId}.navi-module-loader"
    android:exported="false"
    android:name=".app.ModuleLoader"/>
複製代碼

ContentProvider#onCreateApplication#attachBaseContext調用以後Application#onCreate調用以前執行,而且能夠經過getContext拿到ApplicationContext.這樣就解決了部分第三方SDK初始化的問題.

6.ARouter是如何實現的?

簡單歸納起來其實也就是兩個知識點:

  • 使用APT註解處理器經過註解生成RouteMeta元數據到指定包下
  • 啓動時掃描Dex指定包下class,加載並緩存路由表,而後在navigation是對path映射到的不一樣類型儘量地抽象出同一套接口

若是還想深刻理解ARouter,可能就須要去讀源碼了.

7.ARouter的缺點

ARouter目前暫時不支持多進程開發,這是我以爲比較遺憾的,但願將來可以支持吧.

8.結語

ARouter的介紹就到此爲止了,若是還想了解ARouter的依賴注入功能請移步Github.

若是喜歡個人文章記得給我點個贊,拜託了,這對我真的很重要.

相關文章
相關標籤/搜索