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:zhilong.liu@aliyun.com">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被使用的更多一些。