既然组件化是大势所趋,作为Android开发,即使你的项目体量用不到也都应该学点,至少通过组件化可以更加深刻的理解到设计模式一些原则。
组件化就是将业务独立较好的模块,进行解耦,实现独立运行和编译,从而可以协同开发,提高开发效率,减少因修改代码给他其他模块引入bug的风险。
但是既然组件之间是完全解耦,那么组件该如何通信呢,传统是通过intent进行通信,路由的方式是什么样的呢,今天我们就详细的介绍下
接下来主角登场了,Arouter。
它是阿里巴巴开源的一个组件,用于组件化的项目中组件之间的通信,实现解耦合。
先简单的说下如何配置,谈谈在配置中的遇到的坑,再讲下如何使用它,。
本文除了一些基础的配置和使用,当然还会有一些高级的使用方法,对路由通信有个全面的总结。
1.Arouter路由配置
在build.gradle中的defaultconfig中配置
javaCompileOptions {
annotationProcessorOptions {
arguments = [AROUTER_MODULE_NAME: project.getName()]
}
}
不配置的话,会报如下错误:
java.lang.RuntimeException: ARouter::Compiler >>> No module name, for more information
还需要依赖:
annotationProcessor 'com.alibaba:arouter-compiler:1.5.1'
否则就无法使用,除了上面两个配置以外,真正的接口的配置在下面:
api('com.alibaba:arouter-api:1.5.1'){
exclude module: 'support-v4'
}
根据自己的需要,排查一些重复的库,我的项目中v4重复了,所以给排除掉。
重点来了,哪些依赖能够放在公共库,哪些不能,Arouter真是个很讲究的, arouter-api这个依赖可以放到公共库中,采用api进行依赖,这样的话就不需要在各个模块依赖了,而上面两个必须在各个模块进行依赖,否则就会报错。
2.Arouter配置中的坑
- 坑1:只要你想使用Arouter的组件,你就必须在组件中,依赖
annotationProcessor 'com.alibaba:arouter-compiler:1.5.1'
否则就会报错。
-
坑2:清除缓存,我一开始配置有问题,最终配置成功了,但是还是包not match的错误,浪费我半天的时间,于是我清除缓存就好了,卧槽。
-
坑3:当你遇到下面错误时,需要在gradle.properties中添加android.enableJetifier=true
ARouter::Compiler An exception is encountered, [null]
- 坑4: 各个组件中的layout中的文件名不能相同,否则他会认为是同一布局文件。
3.Arouter基本使用
既然基础配置讲完了,你是不是很期待怎么是用了呢,好奇它在组件化中起到什么作用,能够满足我们组件化的需求呢。
我们需要对Arouter进行初始化,官网说越早越好,说明初始化还是挺耗时的,所以一般在application进行初始化。
if (isDebug()) {// 这两行必须写在init之前,否则这些配置在init过程中将无效
ARouter.openLog();// 打印日志
ARouter.openDebug();// 开启调试模式(如果在InstantRun模式下运行,必须开启调试模式!线上版本需要关闭,否则有安全风险)
}
ARouter.init(mApplication); // 尽可能早,推荐在Application中初始化
- Route使用方法
使用方法相当简单,通过注解的方式进行的,注解的关键字就是Route,具体使用如下:
第一步,你首先需要在你想启动的activity或者fragment或者service上进行注册,指定一个唯一的路径,若有重复,则无法解析到,报not match的错误。
@Route(path = ArouterPath.COMPONENT_USER_ABOUT_ACTIVITY)
public class About extends Activity {
……
}
注册之后,就相当于你有了地址,别人可以通过path来访问你,那如何访问呢?
第二步,通过注册的路径进行访问
ARouter.getInstance()
.build(ArouterPath.COMPONENT_USER_ABOUT_ACTIVITY)
.navigation();
模块之间的通信,不止在于界面之间的跳转,还需要在跳转时,将一些必要的参数带过去,Arouter路由也是支持的。
让我们来看看,怎么讲这些参数带过去。根据参数的类型,使用不同的with方法就行了,是不是很简单。
ARouter.getInstance().build(ArouterPath.COMPONENT_USER_ABOUT_ACTIVITY)
.withLong("key1", 666L)
.withString("key3", "888")
.withObject("key4", new Test("Jack", "Rose"))
.navigation();
你可能觉得,支持的参数是不是太少了,怎么可能,接下来看看官网提供的文档,你就知道支持的多丰富了。
// 构建标准的路由请求
ARouter.getInstance().build("/home/main").navigation();
// 构建标准的路由请求,并指定分组
ARouter.getInstance().build("/home/main", "ap").navigation();
// 构建标准的路由请求,通过Uri直接解析
Uri uri;
ARouter.getInstance().build(uri).navigation();
// 构建标准的路由请求,startActivityForResult
// navigation的第一个参数必须是Activity,第二个参数则是RequestCode
ARouter.getInstance().build("/home/main", "ap").navigation(this, 5);
// 直接传递Bundle
Bundle params = new Bundle();
ARouter.getInstance()
.build("/home/main")
.with(params)
.navigation();
// 指定Flag
ARouter.getInstance()
.build("/home/main")
.withFlags();
.navigation();
// 获取Fragment
Fragment fragment = (Fragment) ARouter.getInstance().build("/test/fragment").navigation();
// 对象传递
ARouter.getInstance()
.withObject("key", new TestObj("Jack", "Rose"))
.navigation();
// 觉得接口不够多,可以直接拿出Bundle赋值
ARouter.getInstance()
.build("/home/main")
.getExtra();
// 转场动画(常规方式)
ARouter.getInstance()
.build("/test/activity2")
.withTransition(R.anim.slide_in_bottom, R.anim.slide_out_bottom)
.navigation(this);
// 转场动画(API16+)
ActivityOptionsCompat compat = ActivityOptionsCompat.
makeScaleUpAnimation(v, v.getWidth() / 2, v.getHeight() / 2, 0, 0);
// ps. makeSceneTransitionAnimation 使用共享元素的时候,需要在navigation方法中传入当前Activity
ARouter.getInstance()
.build("/test/activity2")
.withOptionsCompat(compat)
.navigation();
// 使用绿色通道(跳过所有的拦截器)
ARouter.getInstance().build("/home/main").greenChannel().navigation();
// 使用自己的日志工具打印日志
ARouter.setLogger();
// 使用自己提供的线程池
ARouter.setExecutor();
他不仅支持一般的参数,还支持bundle,数据序列化,居然还支持跳转动画,居然还可以支持获取某个对象。
对于path格式还是需要特别说下:
path分为两级,group/name,group相当于你模块的分类,name就是你fragment/activity的标识,你可以使用类名,也可以不使用,但是最好能够代表他们,做好见名知意。
比如: 这个路径:/login/login_activity,代表着登录模块下的登录activity。
有细心的同学可能要问,我能通过Arouter进行传传值,那么在接收方如何获取这个值呢?
难道还有之前的方法,会不会觉得有点low呢,但是有些同学是代码重构的,改动起来工作量太大了,不想改行不行,当然可以,全部支持,等有时间了再改也不迟。
但是既然用到了路由,那么我们就采用路由的方法进行获取值吧。
通过 @Autowired进行注解,name就是传值的关键字,值得注意时,还需要在oncreate中进行ARouter.getInstance().inject(this),否则接收不到值。
我们看下例子: 发送数据方:
ARouter.getInstance()
.build(ArouterPath.AROUTER_PATH_LOGIN)
.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
.withString("data", "test")
.navigation();
接收参数方:
@Route(path = ArouterPath.AROUTER_PATH_LOGIN)
public class LoginActivity extends AppCompatActivity {
@Autowired(name = "data")
String param;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
ARouter.getInstance().inject(this);//必须注册,否则param获取不到
setContentView(R.layout.login_ly);
Toast.makeText(getApplicationContext(), "param:+" + param, Toast.LENGTH_LONG).show();
}
}
你看看是不是比之前的方案要方便点,赶快行动起来,支持哪些参数呢,下面是官方文档:
// 为每一个参数声明一个字段,并使用 @Autowired 标注
// URL中不能传递Parcelable类型数据,通过ARouter api可以传递Parcelable对象
@Route(path = "/test/activity")
public class Test1Activity extends Activity {
@Autowired
public String name;
@Autowired
int age;
// 通过name来映射URL中的不同参数
@Autowired(name = "girl")
boolean boy;
// 支持解析自定义对象,URL中使用json传递
@Autowired
TestObj obj;
// 使用 withObject 传递 List 和 Map 的实现了
// Serializable 接口的实现类(ArrayList/HashMap)
// 的时候,接收该对象的地方不能标注具体的实现类类型
// 应仅标注为 List 或 Map,否则会影响序列化中类型
// 的判断, 其他类似情况需要同样处理
@Autowired
List<TestObj> list;
@Autowired
Map<String, List<TestObj>> map;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
ARouter.getInstance().inject(this);
// ARouter会自动对字段进行赋值,无需主动获取
Log.d("param", name + age + boy);
}
}
你可能还有一个痛点:你想使用别的模块的服务,改怎么办呢?难道还去依赖,这样的话组件化的意义也没有了,但是你可能说我把它放在common库中,不就解决了吗,你真机制。
这倒是个解决办法,但是可能考虑到这个服务和模块强相关,放到common中违背了设计模式的原则,Arouter考虑到,也提供了解决这个问题的方法,看看如何使用吧,IProvider。
4.IProvider的使用
我还是通过一个例子介绍它是如何使用吧,现在有两个模块,分别为Module1,module2,现在module1想使用module2的服务。他们公用的库为commonlib。
1.在commonlib中新建一个ILoginProvider并继承IProvider。
public interface ILoginProvider extends IProvider {
void userService();
}
2.在你需要的调用服务的模块新建一个类继承ILoginProvider,也就是在module2中实现上面ILoginProvider,并通过@Route注册path,用于其他模块调用。
@Route(path = ArouterPath.AROUTER_PATH_SERVICE)
public class ILoginProviderImp2 implements ILoginProvider {
private Context mContext;
@Override
public void userService() {
Toast.makeText(mContext, "执行本模块的服务", Toast.LENGTH_SHORT).show();
}
@Override
public void init(Context context) {
mContext = context;
}
}
3.在module1怎么使用呢?
- 通过Autowired注册ILoginProvider,是接口类,不是实现类。
- 对这个activity进行注册ARouter.getInstance().inject(this);
- 使用服务。
public class UserCenterActivity extends AppCompatActivity {
private ArrayList<Fragment> mFragmentArrayList = new ArrayList<>();
@Autowired(name = ArouterPath.AROUTER_PATH_SERVICE)
ILoginProvider mILoginProvider;//是接口类,而不是实现类
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.center_activity);
ARouter.getInstance().inject(this);
mILoginProvider.init(getApplicationContext());
mILoginProvider.userService();//使用module2的服务
}
}
5.IInterceptor的使用
登录是做APP很容易遇到的场景,但是有个痛点就是,未登录时,需要跳转到登录界面,有了IInterceptor就能轻松搞定。
新建一个全局的IInterceptor,只要navigation就会在这地方拦截,我们可以根据地址进行拦截,若是没有登录,则直接跳转过去就可以了。
@Interceptor(priority = 2)
public class LoginInterceptorImpl implements IInterceptor {
@Override
public void process(Postcard postcard, InterceptorCallback callback) {
String path = postcard.getPath();
if (ArouterPath.AROUTER_PATH_LOGIN.equals(path)) {
callback.onInterrupt(null);
} else {
callback.onContinue(postcard);
}
}
@Override
public void init(Context context) {
}
}
这个使用起来确实爽,值得拥有。
参考: