Android路由框架ARouter的集成、基本使用以及踩坑全过程
对项目进行过组件化的同学肯定也都经历过这样的痛苦,在模块之间通过原生路由方案的界面跳转存在很多的约束,例如子模块向主模块显示跳转无法引用类依赖,又或者是隐式跳转时繁琐的规则定义。并且在项目中如果涉及到需要根据用户的角色或者权限来展示不同内容时,就会在各个界面产生大量的逻辑代码,后期很难进行统一维护,因此,一套类似于前端的路由框架就能解决我们这一系列的烦恼,而对于Android,如今其实已经有相当多成熟的路由框架了,刚好这次项目中准备使用阿里开发的ARouter框架,因此详细来说说这个框架的集成、使用和一些踩坑的过程。
ARouter官方项目传送门
1.集成配置及初始化框架
1.1添加依赖
在需要集成的module中添加依赖和配置
android {
defaultConfig {
...
javaCompileOptions {
annotationProcessorOptions {
arguments = [moduleName :project.getName() ]
} }
}
}
dependencies {
api 'com.alibaba:arouter-api:1.3.1'
annotationProcessor 'com.alibaba:arouter-compiler:1.1.4'
}
这里顺便说一下 implementation 和 api 关键字,在Android studio3.0版本中,曾经的 compile 关键字被弃用,而 api 则是 compile 的替代品, api 与 compile 没有区别。但最新官方推荐使用 implementation 来代替 compile 关键字,据说 implementation 会使Android studio的编译速度更快呦。
而 implementation 和 api 关键字的区别则在于用 implementation 来声明的依赖包只限于当前module内部使用,对于依赖其module的模块是无法使用到该依赖包的。而用 api 来声明依赖包时,依赖于该module的模块可以正常使用其模块内的依赖包。
除此之外 testCompile 要用 testImplementation 或 testApi 替换,androidTestCompile 要用 androidTestImplementation 或 androidTestApi 替换。
在这里,由于我是将其放入一个公共的module,来让app module进行依赖,因此使用 api 关键字。若没有对项目进行组件化,则可以使用 implementation 关键字进行依赖。
1.2初始化SDK
//初始化ARouter框架
if (isDebugARouter) {
//下面两行必须写在init之前,否则这些配置在init中将无效
ARouter.openLog();
//开启调试模式(如果在InstantRun模式下运行,必须开启调试模式!
// 线上版本需要关闭,否则有安全风险)
ARouter.openDebug();
}
ARouter.init((Application) mContext);
2.ARouter的简单使用
2.1界面跳转
目标Activity添加注释
@Route(path = "/app/login")
public class LoginActivity extends AppCompatActivity {
跳转语句,路由路径建议写成常量,创建路由表进行统一管理。
ARouter.getInstance().build("/app/login").navigation();
如果像我一样对项目进行了组件化的同学就会发现,此时跳转并没有成功,而是弹出错误提示。
这是因为组件化后,即时我们使用了 api 作为依赖的关键字,但仍需在使用ARouter的其他module中配置代码android {
defaultConfig {
...
javaCompileOptions {
annotationProcessorOptions {
arguments = [moduleName :project.getName() ]
} }
}
}
dependencies {
annotationProcessor 'com.alibaba:arouter-compiler:1.1.4'
}
否则无法匹配路由,并且在使用withObject方法携带对象时也会报错,这个后面再说,再试一次发现界面成功跳转。关于注释 @Route 的 path 参数,也需要注意规范,必须要以“/”开头,并且路径至少为两级,不然会编译不通过或者报错
2.2携带基本参数的界面跳转
使用方法如下,传入键值对
Bundle bundle = new Bundle();
bundle.putString("bundleStringKey", "bundleStringValue");
ARouter.getInstance().build("/app/login")
.withString("stringKey", "stringValue")
.withInt("intKey", 100)
.withBoolean("booleanKey", true)
.withBundle("bundle", bundle)
.navigation();
目标界面使用 @Autowired 注解进行注入
@Route(path = "/app/login")
public class LoginActivity extends AppCompatActivity {
@Autowired
String stringKey;
@Autowired
int intKey;
@Autowired
boolean booleanKey;
@Autowired
Bundle bundle;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_login);
//注入ARouter
ARouter.getInstance().inject(this);
Log.e(TAG, stringKey + "..." + intKey + "..." + booleanKey);
Log.e(TAG, bundle.getString("bundleStringKey"));
}
}
注意:注入的属性名要和之前携带的key值完全相同,并且要在需要注入的界面通过ARouter.getInstance().inject(this)注入ARouter,否则无法注入成功。建议将ARouter.getInstance().inject(this)操作放在BaseActivity的onCreate方法中进行。既然有注入,就一定有资源的释放,因此释放资源在Application中进行
@Override
public void onTerminate() {
super.onTerminate();
CommonApplication.getInstance().destroy(mContext);
}
如果释放资源放在BaseActivity的onDestroy方法中进行会报错。最终得到打印结果:
2.3携带对象的界面跳转
2.3.1携带序列化对象的界面跳转
携带 Serializable 和 Parcelable 序列化的对象
TestSerializableBean serializableBean = new TestSerializableBean();
serializableBean.setName("serializable");
TestParcelableBean parcelableBean = new TestParcelableBean();
parcelableBean.setName("parcelable");
ARouter.getInstance().build("/app/login")
.withParcelable("parcelableBean", parcelableBean)
.withSerializable("serializableBean", serializableBean)
.navigation();
目标界面
@Autowired
TestParcelableBean parcelableBean;
@Autowired
TestSerializableBean serializableBean;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_login);
Log.e(TAG, parcelableBean + "");
Log.e(TAG, serializableBean + "");
}
我们发现Serializable序列化的对象为 null,我们查看withSerializable方法发现其被装进了Bundle
public Postcard withSerializable(@Nullable String key, @Nullable Serializable value) {
mBundle.putSerializable(key, value);
return this;
}
因此换一种方法来取发现打印成功
TestSerializableBean serializableBean = (TestSerializableBean) getIntent().getExtras().getSerializable("serializableBean");
Log.e(TAG, serializableBean + "");
2.3.2携带无序列化对象的界面跳转
没有进行过序列化的对象也可以通过withObject对象进行传递,接收方式相同
NormalTest normalTest = new NormalTest();
normalTest.setName("normal");
ARouter.getInstance().build("/app/login")
.withObject("normalTest", normalTest)
.navigation();
但是我们直接使用该方法运行会报错,分析源码发现该方法中用到了SerializationService
public Postcard withObject(@Nullable String key, @Nullable Object value) {
serializationService = ARouter.getInstance().navigation(SerializationService.class);
mBundle.putString(key, serializationService.object2Json(value));
return this;
}
因此我们需要实现该服务
@Route(path = "/service/json")
public class JsonServiceImpl implements SerializationService {
private Gson gson;
@Override
public T json2Object(String input, Class clazz) {
return gson.fromJson(input, clazz);
}
@Override
public String object2Json(Object instance) {
return gson.toJson(instance);
}
@Override
public T parseObject(String input, Type clazz) {
return gson.fromJson(input, clazz);
}
@Override
public void init(Context context) {
gson = new Gson();
}
}
我们可以在里面定义所需的json解析器,再次运行成功打印该对象。那序列化的对象可以使用该方法传递吗?
TestParcelableBean objParcelableBean = new TestParcelableBean();
objParcelableBean.setName("objParcelable");
TestSerializableBean objSerializableBean = new TestSerializableBean();
objSerializableBean.setName("objSerializable");
NormalTest normalTest = new NormalTest();
normalTest.setName("normal");
ARouter.getInstance().build("/app/login")
.withObject("objParcelableBean", objParcelableBean)
.withObject("objSerializableBean", objSerializableBean)
.withObject("normalTest", normalTest)
.navigation();
//目标界面
@Autowired(name = "objParcelableBean")
TestParcelableBean objParcelableBean;
@Autowired(name = "objSerializableBean")
TestSerializableBean objSerializableBean;
@Autowired(name = "normalTest")
NormalTest normalTest;
Log.e(TAG, objParcelableBean + "");
Log.e(TAG, objSerializableBean + "");
Log.e(TAG, normalTest + "");
我们发现用 Parcelable 序列化的对象为空,分析build的编译文件
@Override
public void inject(Object target) {
serializationService = ARouter.getInstance().navigation(SerializationService.class);
LoginActivity substitute = (LoginActivity)target;
substitute.objParcelableBean = substitute.getIntent().getParcelableExtra("objParcelableBean");
if (null != serializationService) {
substitute.objSerializableBean = serializationService.parseObject(substitute.getIntent().getStringExtra("objSerializableBean"), new com.alibaba.android.arouter.facade.model.TypeWrapper(){}.getType());
} else {
Log.e("ARouter::", "You want automatic inject the field 'objSerializableBean' in class 'LoginActivity' , then you should implement 'SerializationService' to support object auto inject!");
}
if (null != serializationService) {
substitute.normalTest = serializationService.parseObject(substitute.getIntent().getStringExtra("normalTest"), new com.alibaba.android.arouter.facade.model.TypeWrapper(){}.getType());
} else {
Log.e("ARouter::", "You want automatic inject the field 'normalTest' in class 'LoginActivity' , then you should implement 'SerializationService' to support object auto inject!");
}
}
我们可以看到唯独通过 Parcelable 方式序列化的对象没有使用SerializationService进行解析,而是直接从Bundle去取,但我们并不是通过withParcelable方法去设置的值,因此取得的数据为null。
小结:因此,为了方便我们的操作,没有序列化和使用 Serializable 序列化的对象使用 withObject 方法传递,使用 Parcelable 方式序列化的对象则采用 withParcelable 方法进行传递。
2.3.3携带集合和数组的界面跳转
集合和数组的界面跳转统一使用 withObject 方法传递,并且能够支持成员的各种序列化方式。
List listNormal = new ArrayList<>();
listNormal.add(new NormalTest());
listNormal.add(new NormalTest());
List listSerializable = new ArrayList<>();
listSerializable.add(new TestSerializableBean());
listSerializable.add(new TestSerializableBean());
List listParcelable = new ArrayList<>();
listParcelable.add(new TestParcelableBean());
listParcelable.add(new TestParcelableBean());
Map map = new HashMap<>();
map.put("1", new NormalTest());
map.put("2", new NormalTest());
ARouter.getInstance().build("/app/login")
.withObject("listNormal", listNormal)
.withObject("listSerializable",listSerializable)
.withObject("listParcelable",listParcelable)
.withObject("map", map)
.navigation();
//目标界面
@Autowired
List listNormal;
@Autowired
List listSerializable;
@Autowired
List listParcelable;
@Autowired
Map map;
Log.e(TAG, listNormal + "");
Log.e(TAG, listSerializable + "");
Log.e(TAG, listParcelable + "");
Log.e(TAG, map + "");
2.4界面跳转回调
//启动界面
ARouter.getInstance().build("/app/login")
.navigation(MainActivity.this, REQUEST_CODE);
@Override
protected void onActivityResult(int requestCode, int resultCode, @Nullable Intent data) {
super.onActivityResult(requestCode, resultCode, data);
if (requestCode == REQUEST_CODE&& resultCode == RESULT_CODE) {
LogUtils.e(data.getStringExtra("data"));
}
}
//目标界面
Intent intent = new Intent();
intent.putExtra("data", "resultData");
setResult(RESULT_CODE, intent);
finish();
有同学说resultCode的值为0,我想说这和框架无关,一定是你setResult后没有进行finish操作导致的。
3.ARouter获取fragment实例
有的人给我说ARouter可以跳转fragment,还在想是怎么一回事,看了之后发现其实是获取fragment的实例。用法和跳转Activity类似,只需要把结果进行强转即可。
//目标界面
@Route(path = "/app/fragment")
public class EmptyFragment extends BaseFragment {
}
//启动界面
Fragment fragment= (Fragment) ARouter.getInstance().build("/app/fragment").navigation();
FragmentManager manager = getSupportFragmentManager();
FragmentTransaction transaction = manager.beginTransaction();
transaction.add(R.id.fl_fragment_content, fragment);
transaction.commit();
4.ARouter拦截器
也可以设置拦截器,对跳转的路由进行统一的控制,这就非常适合我们实现不同角色权限对应的不同展示效果,而且后期维护起来也比较方便,不会在各个界面中产生大量跳转逻辑。
我们先来定义两个拦截器:
@Interceptor(priority = 1)
public class FirstRouterInterceptor implements IInterceptor {
@Override
public void init(Context context) {
LogUtils.e("first init");
}
@Override
public void process(Postcard postcard, InterceptorCallback callback) {
LogUtils.e("first process start");
callback.onContinue(postcard);
LogUtils.e("first process end");
}
}
//-------------------------------------------------------------------------
@Interceptor(priority = 2)
public class SecondRouterInterceptor implements IInterceptor {
@Override
public void init(Context context) {
LogUtils.e("second init");
}
@Override
public void process(Postcard postcard, InterceptorCallback callback) {
LogUtils.e("second process start");
callback.onContinue(postcard);
LogUtils.e("second process end");
}
}
可以在跳转逻辑中添加回调,方便我们控制跳转逻辑:
ARouter.getInstance().build("/app/login")
.navigation(mContext, new NavigationCallback() {
@Override
public void onFound(Postcard postcard) {
LogUtils.e("", postcard.getGroup(), postcard.getPath(), "onFound");
}
@Override
public void onLost(Postcard postcard) {
LogUtils.e("", postcard.getGroup(), postcard.getPath(), "onLost");
}
@Override
public void onArrival(Postcard postcard) {
LogUtils.e("", postcard.getGroup(), postcard.getPath(), "onArrival");
}
@Override
public void onInterrupt(Postcard postcard) {
LogUtils.e("", postcard.getGroup(), postcard.getPath(), "onInterrupt");
}
});
看下打印效果:
我们可以看到对于拦截器的 priority 属性,值越小,优先级越高,并且需要注意,该属性的值不能设置为相同的,否则编译时会直接报错。
整个流程我们会按照优先级先初始化两个拦截器,当我们的路由被发现时,拦截器按照优先级开始进行拦截,若都通过,最后到达我们指定的界面。现在我们来模拟一下两个拦截器分别进行拦截的打印效果,我们可以通过把callback.onContinue(postcard);改为callback.onInterrupt(new Throwable());来进行拦截,我们先看下SecondRouterInterceptor被拦截的效果:
我们看到,这时界面已经无法成功跳转,回调触发了onInterrupt方法。并且,即使SecondRouterInterceptor进行了拦截,拦截之后,依旧会回到FirstRouterInterceptor拦截器中执行剩下的代码,并非直接阻断。
我们恢复对SecondRouterInterceptor进行的拦截,将FirstRouterInterceptor拦截来看下效果:
页面依旧无法进行跳转,并且不会再触发SecondRouterInterceptor拦截器。
5.ARouter转场动画
使用ARouter跳转也可以直接给界面设置转场动画
ARouter.getInstance().build(RouterUrl.ACTIVITY_URL_LOGIN)
.withTransition(R.anim.dialog_bottom_in, R.anim.dialog_bottom_out)
.navigation();
但是我们发现,设置的转场动画并没有生效,这是怎么一回事?我们看下源码:
// Navigation in main looper.
new Handler(Looper.getMainLooper()).post(new Runnable() {
@Override
public void run() {
if (requestCode > 0) { // Need start for result
ActivityCompat.startActivityForResult((Activity) currentContext, intent, requestCode, postcard.getOptionsBundle());
} else {
ActivityCompat.startActivity(currentContext, intent, postcard.getOptionsBundle());
}
if ((0 != postcard.getEnterAnim() || 0 != postcard.getExitAnim()) && currentContext instanceof Activity) { // Old version.
((Activity) currentContext).overridePendingTransition(postcard.getEnterAnim(), postcard.getExitAnim());
}
if (null != callback) { // Navigation over.
callback.onArrival(postcard);
}
}
});
就是注释写的Old version.的那一段,网上很多人说这是为了兼容老版本,那我们换一种方式再试下:
ActivityOptionsCompat compat = ActivityOptionsCompat
.makeCustomAnimation(mContext, R.anim.dialog_bottom_in, R.anim.dialog_bottom_out);
ARouter.getInstance().build(RouterUrl.ACTIVITY_URL_LOGIN)
.withOptionsCompat(compat)
.navigation();
我们发现入场动画生效了,难道真的是 withTransition 方法过时了嘛?但是并没有标注这是过时方法啊?我们再仔细看下这段源码:
if ((0 != postcard.getEnterAnim() || 0 != postcard.getExitAnim()) && currentContext instanceof Activity) { // Old version.
((Activity) currentContext).overridePendingTransition(postcard.getEnterAnim(), postcard.getExitAnim());
}
是上下文引用 overridePendingTransition 方法设置的出入场动画,需要设置上下文!对了!如果不设置上下文,框架会使用一个自定义的context,但是无法作用在目标界面上,我们把跳转代码改成这样:
ARouter.getInstance().build(RouterUrl.ACTIVITY_URL_LOGIN)
.withTransition(R.anim.dialog_bottom_in, R.anim.dialog_bottom_out)
.navigation(MainActivity.this);
入场动画生效。这里需要注意一下,无论用哪种方法,出场动画都不会生效,所以在设置出场动画时,建议在目标界面添加如下代码设置出场动画。
@Override
public void finish() {
super.finish();
overridePendingTransition(R.anim.anim_none, R.anim.dialog_bottom_out);
}
6.ARouter自定义分组
定义分组比较简单,首先在注释上多加一个group属性
@Route(path = RouterUrl.ACTIVITY_URL_LOGIN, group = "group")
然后跳转时携带分组信息
ARouter.getInstance().build(RouterUrl.ACTIVITY_URL_LOGIN,"group")
.navigation(SplashActivity.this);
如果目标界面设置了分组属性,在跳转时一定要携带分组信息,否则会找不到该路由。另外,该方法已经过时了,建议用下列方法设置分组信息:
Postcard build = ARouter.getInstance().build(RouterUrl.ACTIVITY_URL_LOGIN);
build.setGroup("group");
build.navigation(SplashActivity.this);
7.小结
再顺带一提这篇文章是如何诞生的,因为在踩坑的过程中去查阅了一些文章,大概是由于框架更新的原因,很多老文章提出的方案并不能解决我所有遇到的问题,而且还有很多文章地址失效,所以导致我踩坑花费了较多的精力。但这些文章依旧提供给我解决问题的思路,而在写文章的过程中也使我能更加清晰的梳理思路和深刻的了解原理,因此使我意识到写文章带来的多重好处,也希望我这篇文章能给使用该框架的开发者们带来帮助,并向写文章帮助到我的各位前辈们致敬。
8.相关参考文献
探索Android路由框架-ARouter之基本使用
ARouter组件化之路遇到的坑
Arouter->withSerializable传值失败源码解析