ARouter入門使用篇
Android原生的路由方案是通過Intent來實現顯式和隱式兩種Activity跳轉方案,顯式Intent需要對目標Activity直接應用,會導致不同頁面直接存在耦合的情況,隱式Intent存在Action集中配置在Manifest中,不便於管理的問題。而且在組件化開發中,各模塊無法直接相互引用,路由跳轉管理問題變成了必須要解決的問題。
ARouter是阿里開源的Android端路由框架,就是爲了解決組件化開發中的路由跳轉問題而被開發出來的。
一個用於幫助 Android App 進行組件化改造的框架 —— 支持模塊間的路由、通信、解耦
GitHub項目地址:ARouter
淺談路由框架原理
我們以組件化開發中Activity跳轉爲例,簡單聊一下路由框架的實現原理。無論上層框架如何封裝,activity的底層跳轉總是要通過startActivity()
實現的,那麼就需要獲取到目標Activity的實例或路徑。爲了實現模塊間解耦,又不能直接引用目標Activity,最簡單的辦法就是給目標Activity設置一個簡單的別名,然後通過映射表的方式Map<string, calss<>>
維護別名與Activity的關係,那麼這個映射表的實現只能下沉到所以模塊都引用的基礎模塊中,比如base中。那麼整個流程就很清楚了:
Activity提前將映射關係注入到Map中,當AActivity發起跳轉到B的請求時,基礎模塊會從映射表中查找對應的Activity實例,然後進行跳轉。如果找不到對應的Activity實例,可以將跳轉結果回傳避免引起異常。
ARouter的使用
添加依賴
android {
defaultConfig {
...
javaCompileOptions {
annotationProcessorOptions {
arguments = [AROUTER_MODULE_NAME: project.getName()]
}
}
}
}
dependencies {
// 替換成最新版本, 需要注意的是api
// 要與compiler匹配使用,均使用最新版可以保證兼容
compile 'com.alibaba:arouter-api:x.x.x'
annotationProcessor 'com.alibaba:arouter-compiler:x.x.x'
...
}
// 舊版本gradle插件(< 2.2),可以使用apt插件,配置方法見文末'其他#4'
// Kotlin配置參考文末'其他#5'
目前最新版本(2021年12月7日)是1.5.2版本,以下介紹皆基於此版本說明。
混淆
添加混淆規則(如果使用了Proguard)
-keep public class com.alibaba.android.arouter.routes.**{*;}
-keep public class com.alibaba.android.arouter.facade.**{*;}
-keep class * implements com.alibaba.android.arouter.facade.template.ISyringe{*;}
# 如果使用了 byType 的方式獲取 Service,需添加下面規則,保護接口
-keep interface * implements com.alibaba.android.arouter.facade.template.IProvider
# 如果使用了 單類注入,即不定義接口實現 IProvider,需添加下面規則,保護實現
# -keep class * implements com.alibaba.android.arouter.facade.template.IProvider
初始化
官方建議儘早初始化,放到Application中
if (isDebug()) {
// 這兩行必須寫在init之前,否則這些配置在init過程中將無效
ARouter.openLog(); // 打印日誌
ARouter.openDebug(); // 開啓調試模式(如果在InstantRun模式下運行,必須開啓調試模式!線上版本需要關閉,否則有安全風險)
}
ARouter.init(mApplication); // 儘可能早,推薦在Application中初始化
在開發調試過程中注意需要調用openDebug()
方法,否則可能會出現找不到Activity的情況。
添加註解
在目標Activity/Fragment中用Router
添加註解,path爲路由路徑
@Router(path = "/app/MainActivity")
public class MainActivity extends Activity{
protected void onCreate(){
...
//注入
ARouter.getInstance().inject(this)
}
}
路徑必須用/
且至少有兩級,同時需要在onCreate中進行注入。
發起路由
-
Activity跳轉
ARouter.getInstance().build("/app/MainActivity").navigation();
-
Fragment
aFragment = (AFragment) ARouter.getInstance().build("/fragment/AFragment").navigation();
傳參
-
基本數據類型
傳遞參數:
ARouter.getInstance().build(路徑) .withChar("CharKey", 'a') .withShort("ShortKey", 1) .withInt("IntKey", 11) .withLong("LongKey", 12l) .withFloat("FloatKey", 1.1f) .withDouble("DoubleKey", 1.1d) .withBoolean("BooleanKey", true) .withString("StringKey", "value") .navigation();
通過註解解析參數:
注意不能使用private關鍵字修飾。
//僞代碼如下 public class TestActivity extends Activity{ //用Autowired註解 @Autowired(name = "IntKey") int i; @Autowired(name = "StringKey") String str; //...省略其他 onCreate(){ ... Log.d("IntKey = " + i + " StringKey = " + str); } }
傳參過程中,可以省略
name
ARouter會自動根據類型匹配參數,但是建議都指定name
,避免一些異常。 -
序列化對象
傳遞:
ARouter.getInstance().build("路徑") .withSerializable("SerializableKey", new TestObj()) .withParcelable("ParcelableKey", new TestObj()) .navigation();
傳遞對接需要分別實現
Serializable
和Parcelable
接口,調用的方法與基本數據類型大同小異,很好理解。解析:
同樣通過註解進行自動參數解析。
// 支持解析自定義對象,URL中使用json傳遞 @Autowired(name = "ParcelableKey") TestObj obj;
同樣建議指定
name
,當然也支持省略name
。 -
自定義對象
傳遞自定義對象的前提是對象不能實現
Serializable
或Parcelable
接口。傳遞:
ARouter.getInstance().build("路徑") .withObject("ObjectKey", new TestObjectBean()) .navigation();
解析:
@Autowired(name = "ObjectKey") TestObjectBean object;
除了上述兩步外,傳遞自定義對象必須新建一個類,實現 SerializationService,並使用@Route註解標註。
ARouter這麼設計是爲了方便用戶自己選擇Json解析方式。(路徑隨意指定一個不重複的就可以)
@Route(path = "/app/custom/json") public class JsonSerializationService implements SerializationService { Gson gson; @Override public <t> T json2Object(String input, Class<t> clazz) { return gson.fromJson(input,clazz); } @Override public String object2Json(Object instance) { return gson.toJson(instance); } @Override public <t> T parseObject(String input, Type clazz) { return gson.fromJson(input,clazz); } @Override public void init(Context context) { gson = new Gson(); } }
除了初始化
init
方法,其他幾個方法都是用來處理json和object對象轉換的。因爲整個自定義對象的傳遞過程經歷了一下幾步:
- withObhect("", obj)
- 調用SerializationService的2json方法將obj轉換成string的json對象。
- 將string的json對象傳遞到目標頁面。
- 目標頁面調用調用SerializationService的2Object方法將json對象轉換成obj對象。
也可以從
withObject
的源碼中看到轉換步驟:/** * Set object value, the value will be convert to string by 'Fastjson' * * @param key a String, or null * @param value a Object, or null * @return current */ public Postcard withObject(@Nullable String key, @Nullable Object value) { serializationService = ARouter.getInstance().navigation(SerializationService.class); mBundle.putString(key, serializationService.object2Json(value)); return this; }
需要注意的是,因爲參數的解析過程是通過類型匹配自動處理的,所以使用
withObject()
傳遞List和Map對象時,接收該對象時不能指定List和Map的實現了Serializable
的實現類(ArrayList
和HashMap
等),可能有點拗口,簡單來說就是接收withObject傳參的對象,不能是Serializable
或Parcelable
的實現類。(如果指定了Serializable
或Parcelable
的實現類會影響序列化類型的判斷。)
路由管理
上面的跳轉處理過程中不可避免會需要指定很多的路由路徑(如Activity路徑等),爲了方便管理和處理,通常會定義一個常量類去維護和管理所有的路由路徑,並且爲了各模塊可以正常引用需要將常量類下沉到基礎模塊中(如BaseModule中),雖然這在一定程度上破壞了各模塊的獨立性(必須依賴常量類模塊才能實現路由跳轉),增加了業務模塊與基礎模塊的耦合性,但是處於代碼維護的角度考慮,這麼做還是有必要的。
/**
* 路由管理類
*/
public class ARouterPath {
/** push module */
public static final String SERVICE_PUSH_RECEIVER_URL = "/push/PushRegisterActivity";
/** Display Module */
public static final String ACTIVITY_DISPLAY_DISPLAY_URL = "/display/DisplayActivity";
public static final String SERVICE_DISPLAY_UPLOAD_URL = "/display/PushReceiver";
}
攔截器
之前在介紹OkHttp中提到過,OkHttp的攔截器設計是整個框架實現中特別經典,特別有亮點的部分。在ARouter中同樣添加了攔截器的實現,攔截器可以對路由的過程進行攔截和處理。
@Interceptor(priority = 8, name = "測試攔截器")
public class TestInterceptor implements IInterceptor {
private static final String TAG = "Interceptor";
@Override
public void process(Postcard postcard, InterceptorCallback callback) {
String threadName = Thread.currentThread().getName();
Log.i(TAG, "攔截器開始執行,線程名稱: " + threadName +
"\n postcard = " + postcard.toString());
//攔截路由操作
callback.onInterrupt(new RuntimeException("有異常,禁止路由"));
//繼續路由操作
callback.onContinue(postcard);
}
@Override
public void init(Context context) {
Log.i(TAG, "TestInterceptor攔截器初始化");
}
}
攔截器支持添加多個,通過註解@Interceptor()
進行註解來實現攔截器的註冊,priority
用於定義攔截器的優先級,數字越小優先級越高,多個攔截器按優先級順序執行。
- 多個攔截器不能擁有相同的優先級。
- 用
name
屬性爲攔截器指定名稱(可省略)。 init()
方法會在攔截器被初始化時自動調用。process()
當有路由操作被髮起時會觸發,可以根據需要通過onInterrupt()
攔截路由,或者通過onContinue()
繼續路由操作,注意兩個方法必須調用一個,否則路由會丟失不會繼續執行。
攔截器比較經典的應用時用來判斷登錄事件,app中某些頁面必須用戶登錄之後才能跳轉,這樣的話就可以通過攔截器做登錄檢查,避免在目標頁面重複檢查。
跳轉結果監聽處理:
ARouter.getInstance().build("路徑")
.navigation(this, new NavigationCallback() {
@Override
public void onFound(Postcard postcard) {
//路由發現
}
@Override
public void onLost(Postcard postcard) {
//路由丟失
}
@Override
public void onArrival(Postcard postcard) {
//達到
}
@Override
public void onInterrupt(Postcard postcard) {
//攔截
}
});
在跳轉時通過指定NavigationCallback
進行跳轉監聽。如果只想監聽到達事件也可以通過指定抽象類NavCallback
來進行簡化。
需要注意的是隻有Activity纔會觸發攔截器,Fragment和IProvider並不支持攔截。
通過查看源碼,可以發現攔截處理是在_ARouter的navigation()
方法中處理的。
if (!postcard.isGreenChannel()) { // It must be run in async thread, maybe interceptor cost too mush time made ANR.
//處理攔截器
interceptorService.doInterceptions(postcard, new InterceptorCallback() {
/**
* Continue process
*
* @param postcard route meta
*/
@Override
public void onContinue(Postcard postcard) {
_navigation(postcard, requestCode, callback);
}
//省略
}
是否被攔截取決於postcard.isGreenChannel()
值,而賦值是在LogisticsCenter的completion()
方法中:
switch (routeMeta.getType()) {
case PROVIDER: // if the route is provider, should find its instance
// Its provider, so it must implement IProvider
Class<!--? extends IProvider--> providerMeta = (Class<!--? extends IProvider-->) routeMeta.getDestination();
IProvider instance = Warehouse.providers.get(providerMeta);
if (null == instance) { // There's no instance of this provider
IProvider provider;
try {
provider = providerMeta.getConstructor().newInstance();
provider.init(mContext);
Warehouse.providers.put(providerMeta, provider);
instance = provider;
} catch (Exception e) {
logger.error(TAG, "Init provider failed!", e);
throw new HandlerException("Init provider failed!");
}
}
postcard.setProvider(instance);
//Provider不需要攔截
postcard.greenChannel(); // Provider should skip all of interceptors
break;
case FRAGMENT:
//Fragment不需要攔截
postcard.greenChannel(); // Fragment needn't interceptors
default:
break;
}
通過上面的源碼可以看到類型判斷是通過routeMeta.getType()
很明顯就是路由的類型,而ARouter雖然定義了很多的類型:
/**
* Type of route enum.
*
* @author Alex <a href="mailto:[email protected]">Contact me.</a>
* @version 1.0
* @since 16/8/23 22:33
*/
public enum RouteType {
ACTIVITY(0, "android.app.Activity"),
SERVICE(1, "android.app.Service"),
PROVIDER(2, "com.alibaba.android.arouter.facade.template.IProvider"),
CONTENT_PROVIDER(-1, "android.app.ContentProvider"),
BOARDCAST(-1, ""),
METHOD(-1, ""),
FRAGMENT(-1, "android.app.Fragment"),
UNKNOWN(-1, "Unknown route type");
//省略
}
比如在RouteType中定義的如:Service、ContentProvider甚至Boardcast等類型,但是ARouter的路由跳轉其實只支持:Activity、Fragment和IProvider三種類型,其他類型不支持(基於1.5.2版本),IProvider是ARouter的服務組件功能後面會提到。
分組
上面提到了可以通過攔截器來進行登錄判斷,進而攔截頁面,但是這樣的話我們需要在攔截器中將所有需要登錄後跳轉的頁面全部判斷,而分組功能可以將這個問題簡化,將所有需要登錄後跳轉的頁面指定同一個分組,攔截器中只需要判斷分組即可。
//指定分組通過group關鍵字指定
@Route(path = "/app/MainActivity", group = "app")
public class MainActivity extends AppCompatActivity {
//省略
}
//攔截判斷
@Interceptor(priority = 8, name = "測試攔截器")
public class TestInterceptor implements IInterceptor {
@Override
public void process(Postcard postcard, InterceptorCallback callback) {
//獲取分組
String group = postcard.getGroup();
//省略
}
//省略
}
需要注意,如果要跳轉通過group
關鍵字指定的分組的目標頁面,需要顯示指定對應分組,要不然會找不到對應路由。
ARouter.getInstance().build("/app/TextActivity", "test").navigation();
當然瞭如果沒有通過group
指定分組,會默認將path
中的第一個/
後的路徑作爲group,這也是爲什麼path
必須至少兩級//
的原因。
爲目標頁面聲明更多信息
// 我們經常需要在目標頁面中配置一些屬性,比方說"是否需要登陸"之類的
// 可以通過 Route 註解中的 extras 屬性進行擴展,這個屬性是一個 int值,換句話說,單個int有4字節,也就是32位,可以配置32個開關
// 剩下的可以自行發揮,通過字節操作可以標識32個開關,通過開關標記目標頁面的一些屬性,在攔截器中可以拿到這個標記進行業務邏輯判斷
@Route(path = "/test/activity", extras = Consts.XXXX)
簡單來說,可以爲目標頁面指定簡單的屬性,比如官方說的登錄管理功能,其實不是所有的頁面都需要進行登錄判斷,當然我們可以通過上面分組的方式,將需要登錄判斷的頁面劃分都統一分組中login,然後在攔截器中判斷分組,再根據當前登錄狀態進行攔截。
也可以通過extras方式,爲目標頁面指定屬性,如:需要進行登錄校驗的頁面,extras統一設置爲1,不需要登錄校驗的設置爲2或不設置。
@Override
public void process(Postcard postcard, InterceptorCallback callback) {
String threadName = Thread.currentThread().getName();
Log.i(TAG, "攔截器開始執行,線程名稱: " + threadName +
"\n postcard = " + postcard.toString());
//模擬登錄狀態
boolean isLogin = false;
int extras = postcard.getExtra();
//需要登錄校驗,並且未登錄的頁面進行攔截
if(extras == 1 && !isLogin){
callback.onInterrupt(new RuntimeException("請先登錄"));
} else{
callback.onContinue(postcard);
}
}
按照官方說的extras是單個int有4字節,也就是32位,可以配置32個開關,可以根據需要自由配置。
startActivityForResult
ARouter也支持startActivityForResult()
的方式獲取目標頁面的回傳值。
//路由跳轉時需要指定RequestCode,123
ARouter.getInstance().build("/AModule/AModuleActivity").navigation(this, 123);
目標頁面在退出時需要通過setResult方法回傳ResultCode和數據
//目標頁面實現,省略其他代碼
@Override
public void onBackPressed() {
Intent data = new Intent();
data.putExtra("name", "我是AModuleActivity");
setResult(321, data);
super.onBackPressed();
}
同時需要在跳轉頁面,重寫onActivityResult()
方法,整個使用過程也原生startActivityForResult()
基本一致。
//跳轉頁面實現,其他代碼省略
@Override
protected void onActivityResult(int requestCode, int resultCode, @Nullable Intent data) {
super.onActivityResult(requestCode, resultCode, data);
if(requestCode == 123 && resultCode == 321){
String name = data.hasExtra("name")? data.getStringExtra("name") : "";
Log.d(TAG, "onActivityResult: name = " + name);
}
}
動態增加路由
上面提到的所有頁面的路由添加都是通過註解關鍵字@Route
和注入方法inject()
在代碼編譯時ARouter框架自動添加的。ARouter同樣支持動態路由的增加。
ARouter.getInstance().addRouteGroup(new IRouteGroup() {
@Override
public void loadInto(Map<string, routemeta> atlas) {
atlas.put("/dynamic/activity", // path
RouteMeta.build(
RouteType.ACTIVITY, // 路由信息
TestDynamicActivity.class, // 目標的 Class
"/dynamic/activity", // Path
"dynamic", // Group, 儘量保持和 path 的第一段相同
0, // 優先級,暫未使用
0 // Extra,用於給頁面打標
)
);
}
});
這種方式適用於部分插件化開發場景中,添加路由之後的跳轉過程與之前一致。
服務
前面也提到了IProvider服務組件功能,是通過實現IProvider
接口來定義組件內的開放接口,來滿足其他組件的調用需求。通俗點就是AModule中通過服務開放功能,讓其他Module可以調用。
-
實現IProvider接口
通過IProvider接口定義ARouter服務。
// 聲明接口,其他組件通過接口來調用服務 public interface HelloService extends IProvider { String sayHello(String name); } // 實現接口 @Route(path = "/yourservicegroupname/hello", name = "測試服務") public class HelloServiceImpl implements HelloService { @Override public String sayHello(String name) { return "hello, " + name; } @Override public void init(Context context) { } }
在組件化開發中,通常會在底層模塊中定義一個接口類
HelloService
(比如在base模塊中),再具體模塊中實現接口功能。注意:服務只有在被使用時纔會觸發init方法被初始化。
-
發現服務
-
依賴注入
public class Test { @Autowired HelloService helloService; @Autowired(name = "/yourservicegroupname/hello") HelloService helloService2; public void fun(){ //可以直接使用服務 // 1. (推薦)使用依賴注入的方式發現服務,通過註解標註字段,即可使用,無需主動獲取 // Autowired註解中標註name之後,將會使用byName的方式注入對應的字段,不設置name屬性,會默認使用byType的方式發現 //服務(當同一接口有多個實現的時候,必須使用byName的方式發現服務) helloService.sayHello("1"); helloService2.sayHello("2"); } }
-
依賴查找
public class Test { @Autowired HelloService helloService; @Autowired(name = "/yourservicegroupname/hello") HelloService helloService2; public void fun(){ // 2. 使用依賴查找的方式發現服務,主動去發現服務並使用,下面兩種方式分別是byName和byType helloService3 = ARouter.getInstance().navigation(HelloService.class); helloService4 = (HelloService) ARouter.getInstance().build("/yourservicegroupname/hello").navigation(); helloService3.sayHello("Vergil"); helloService4.sayHello("Vergil"); } }
-
預處理服務
// 實現 PretreatmentService 接口,並加上一個Path內容任意的註解即可 @Route(path = "/xxx/xxx") public class PretreatmentServiceImpl implements PretreatmentService { @Override public boolean onPretreatment(Context context, Postcard postcard) { // 跳轉前預處理,如果需要自行處理跳轉,該方法返回 false 即可 } @Override public void init(Context context) { } }
-
小結
ARouter的所有功能和使用方式基本介紹完了。類似的路由框架還有
感興趣的可以瞭解一下,不過目前看還是ARouter被使用的更多一些。