1.為什麼要元件化?
國內都比較流行開發超級APP,也就是我全都要,什麼功能都想加進去,這導致業務邏輯變得越來越複雜.
這時我們會開始面臨兩個問題:
- 首先,我們的
res
資料夾下的資源將會迎來爆炸式地增長,並且我們都知道res
資料夾不能分層,它只能按module
進行劃分,所以你的layout
和mipmap
等資料夾將最先被迫害,當這兩個資料夾的資源變多時,你要查詢一個layout
或者一張圖片都會變得十分費勁 - 其次,如果此時你的
APP
還是隻有一個module
,還將會可能導致業務邏輯耦合無法複用,除非你的程式設計習慣十分良好,但是絕大多數人都做不到,所以我們需要用元件化
來給自己一些約束
,以此創造更高質量的應用程式.
2.使用ARouter對專案進行元件化改造
我特別喜歡ARouter
簡介中的一句話:解耦不是前提而是過程.接下來我將介紹如何使用ARouter
對專案進行元件化改造
要元件化,首先你需要建立module
來分割你的業務邏輯.要建立新的module
可以在你的project
名字上右鍵,然後New->Module
Android Library
即可.
工程中有一個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
,其他模組只需要我這個模組的地圖功能,但它不應該知道我到底使用的是高德
還是百度
還是騰訊
地圖,這就提高了封裝性,在未來改變此模組的具體實現時,代價也會小得多.
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.
如果喜歡我的文章記得給我點個贊,拜託了,這對我真的很重要.