Android技術棧(二)元件化改造
1.為什麼要元件化?
國內都比較流行開發超級APP,也就是我全都要,什麼功能都想加進去,這導致業務邏輯變得越來越複雜.

image.png
這時我們會開始面臨兩個問題:
- 首先,我們的
res
資料夾下的資源將會迎來爆炸式地增長,並且我們都知道res
資料夾不能分層,它只能按module
進行劃分,所以你的layout
和mipmap
等資料夾將最先被迫害,當這兩個資料夾的資源變多時,你要查詢一個layout
或者一張圖片都會變得十分費勁 - 其次,如果此時你的
APP
還是隻有一個module
,還將會可能導致業務邏輯耦合無法複用,除非你的程式設計習慣十分良好,但是絕大多數人都做不到,所以我們需要用元件化
來給自己一些約束
,以此創造更高質量的應用程式.
2.使用ARouter對專案進行元件化改造
我特別喜歡 ARouter
簡介中的一句話:解耦不是前提而是過程.接下來我將介紹如何使用 ARouter
對專案進行元件化改造
要元件化,首先你需要建立 module
來分割你的業務邏輯.要建立新的 module
可以在你的 project
名字上右鍵,然後 New->Module

image.png
然後選擇 Android Library
即可.

image.png
工程中有一個 host
的 com.android.application
殼 module
,其他包含業務邏輯的 module
以 com.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
模組中定義一個 PR
的 java
檔案,來儲存我們專案中所用到的所有路由的 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
對映到的 Fragment
或 Activity
,而在程式碼修改時把對呼叫方的影響降低到最小.
但值得注意的是, 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
,其他模組只需要我這個模組的地圖功能,但它不應該知道我到底使用的是 高德
還是 百度
還是 騰訊
地圖,這就提高了封裝性,在未來改變此模組的具體實現時,代價也會小得多.

image.png
3.自定義全域性攔截器、全域性降級策略、全域性重定向
ARouter
實現了 module
間的路由操作,同時也實現了攔截器的功能,攔截器是一種 AOP
(面向切面程式設計),比較經典的使用場景就是處理頁面登入與否的問題.攔截器會在跳轉之間執行,多個攔截器會按優先順序順序依次執行.通過實現 IInterceptor
介面並標註 @Interceptor
註解,這樣一來,這個攔截器就被註冊到 ARouter
當中了.
process
方法會傳入 Postcard
和 InterceptorCallback
, 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
模組的 TTSService
和 ai
模組的 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
初始化是必須要在 Application
的 onCreate
中進行初始化的,但是如果我們編寫獨立於 host
的 module
時,要怎麼初始化它們呢?
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#onCreate
在 Application#attachBaseContext
呼叫之後 Application#onCreate
呼叫之前執行,並且可以通過 getContext
拿到 Application
的 Context
.這樣就解決了部分第三方 SDK
初始化的問題.
6.ARouter是如何實現的?
簡單概括起來其實也就是兩個知識點:
- 使用
APT
註解處理器通過註解生成RouteMeta
元資料到指定包下 - 啟動時掃描
Dex
指定包下class
,載入並快取路由表,然後在navigation
是對path
對映到的不同型別儘可能地抽象出同一套介面
如果還想深入理解 ARouter
,可能就需要去讀原始碼了.
7.ARouter的缺點
ARouter
目前暫時不支援多程序開發,這是我覺得比較遺憾的,希望未來能夠支援吧.
8.結語
ARouter
的介紹就到此為止了,如果還想了解 ARouter
的依賴注入功能請移步 Github .
【附】相關架構及資料

image