國內都比較流行開發超級APP,也就是我全都要,什麼功能都想加進去,這致使業務邏輯變得愈來愈複雜. java
這時咱們會開始面臨兩個問題:android
res
文件夾下的資源將會迎來爆炸式地增加,而且咱們都知道res
文件夾不能分層,它只能按module
進行劃分,因此你的layout
和mipmap
等文件夾將最早被迫害,當這兩個文件夾的資源變多時,你要查找一個layout
或者一張圖片都會變得十分費勁APP
仍是隻有一個module
,還將會可能致使業務邏輯耦合沒法複用,除非你的編程習慣十分良好,可是絕大多數人都作不到,因此咱們須要用組件化
來給本身一些約束
,以此創造更高質量的應用程序.我特別喜歡ARouter
簡介中的一句話:解耦不是前提而是過程.接下來我將介紹如何使用ARouter
對項目進行組件化改造git
要組件化,首先你須要建立module
來分割你的業務邏輯.要建立新的module
能夠在你的project
名字上右鍵,而後New->Module
github
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 //業務邏輯
複製代碼
解釋一下: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
模塊中定義一個PR
的java
文件,來保存咱們項目中所用到的全部路由的path
app
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
,其餘模塊只須要我這個模塊的地圖功能,但它不該該知道我到底使用的是高德
仍是百度
仍是騰訊
地圖,這就提升了封裝性,在將來改變此模塊的具體實現時,代價也會小得多.
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
方法,只有攔截器的調用時間是特殊的,其餘兩種,都是在第一次使用時纔會進行初始化.
有的時候咱們可能須要的不是另一個模塊的頁面,而是它提供的服務(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()
複製代碼
有些第三方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
初始化的問題.
簡單歸納起來其實也就是兩個知識點:
APT
註解處理器經過註解生成RouteMeta
元數據到指定包下Dex
指定包下class
,加載並緩存路由表,而後在navigation
是對path
映射到的不一樣類型儘量地抽象出同一套接口若是還想深刻理解ARouter
,可能就須要去讀源碼了.
ARouter
目前暫時不支持多進程開發,這是我以爲比較遺憾的,但願將來可以支持吧.
ARouter
的介紹就到此爲止了,若是還想了解ARouter
的依賴注入功能請移步Github.
若是喜歡個人文章記得給我點個贊,拜託了,這對我真的很重要.