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

_晨曦_發表於2019-03-21

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

1.為什麼要元件化?

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

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

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

  • 首先,我們的res資料夾下的資源將會迎來爆炸式地增長,並且我們都知道res資料夾不能分層,它只能按module進行劃分,所以你的layoutmipmap等資料夾將最先被迫害,當這兩個資料夾的資源變多時,你要查詢一個layout或者一張圖片都會變得十分費勁
  • 其次,如果此時你的APP還是隻有一個module,還將會可能導致業務邏輯耦合無法複用,除非你的程式設計習慣十分良好,但是絕大多數人都做不到,所以我們需要用元件化來給自己一些約束,以此創造更高質量的應用程式.

2.使用ARouter對專案進行元件化改造

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

要元件化,首先你需要建立module來分割你的業務邏輯.要建立新的module可以在你的project名字上右鍵,然後New->Module

Android技術棧(二)元件化改造
然後選擇Android Library即可.
Android技術棧(二)元件化改造
工程中有一個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 //業務邏輯
複製程式碼

解釋一下:

先說common模組,這個模組需要包含專案中要使用的所有依賴和一些公用的工具類,之後每個模組都依賴common模組,這樣就可以把common模組的依賴輕鬆地依賴匯入到其他模組中去而不用在其他模組的build.gradle中重複地寫一大堆指令碼.

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

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

類似R檔案我們還可以在common模組中定義一個PRjava檔案,來儲存我們專案中所用到的所有路由的path

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,其他模組只需要我這個模組的地圖功能,但它不應該知道我到底使用的是高德還是百度還是騰訊地圖,這就提高了封裝性,在未來改變此模組的具體實現時,代價也會小得多.

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

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.

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

相關文章