Android模块化开发与ARouter框架
在App开发的初期,代码量不大,业务量比较少,一个App作为一个单独的模块进行开发,往往问题不大。但随着业务的增多,代码变的越来越复杂,每个模块之间的代码耦合变得越来越严重,结构越来越臃肿,修改一处代码要编译整个工程,导致非常耗时,这时候解耦问题急需解决。
同时,如果公司有多个终端设备的App,而且有块功能是通用的(比如说下单功能),那么通用的这一块功能被复制集成到不同App里,就显得很重复,而且维护时要修改多套代码,严重影响开发效率,因此模块化开发就很有必要。
App模块化的目标是告别结构臃肿,让各个业务变得相对独立,业务模块在组件模式下可以独立开发,而在集成模式下又可以变为依赖包集成到“app壳工程”中,组成一个完整功能的APP。
一、模块化开发的好处
- 公用功能,不用重复开发、修改,代码复用性更强
- 独立运行,提高编译速度,也就提高了开发效率
- 更利于团队开发,不同的人可以独立负责不同的模块
- 独立模块可以采用不同的技术架构,尝试新的技术方案,比如采用新的网络框架,甚至换成Kotlin来开发App
二、模块化要解决的问题
- 模块间页面跳转(路由);
- 模块间事件通信;
- 模块间服务调用;
- 模块的独立运行;
- 模块间页面跳转路由拦截(登录)
三、ARouter路由框架
以上模块化需要要解决的问题,2017年阿里开源的路由框架ARouter都有提供解决方案。
官方对这个框架的定义是:一个用于帮助 Android App 进行组件化改造的框架 —— 支持模块间的路由、通信、解耦。
ARouter提供的功能有:
- 支持直接解析标准URL进行跳转,并自动注入参数到目标页面中
- 支持多模块工程使用
- 支持添加多个拦截器,自定义拦截顺序
- 支持依赖注入,可单独作为依赖注入框架使用
- 支持InstantRun
- 支持MultiDex(Google方案)
- 映射关系按组分类、多级管理,按需初始化
- 支持用户指定全局降级与局部降级策略
- 页面、拦截器、服务等组件均自动注册到框架
- 支持多种方式配置转场动画
- 支持获取Fragment
- 完全支持Kotlin以及混编(配置见文末 其他#5)
- 支持第三方 App 加固(使用 arouter-register 实现自动注册)
- 支持生成路由文档
- 提供 IDE 插件便捷的关联路径和目标类
附上ARouter官网地址:https://github.com/alibaba/ARouter/blob/master/README_CN.md
其中,关于路由方面,Google提供的原生路由主要是通过Intent,Intent可以分成显示和隐式两种。显示的方案会导致类之间的直接依赖问题,耦合严重;隐式Intent需要在配置清单中统一声明,首先有个暴露的问题,另外在多模块开发中协作也比较困难。除此之外,使用原生的路由方案会出现跳转过程无法控制的问题,因为一旦使用了startActivity()就无法插手其中任何环节了,只能交给系统管理,这就导致了在跳转失败的情况下无法降级,而是会直接抛出运营级的异常。
// Intent显式启动Activity
Intent intent = new Intent(this, MainActivity.class);
startActivity(intent);
// Intent隐式启动Activity
Intent intent = new Intent("com.example.activity.ACTION_START");
startActivity(intent);
如果使用ARouter,可以在跳转过程中进行拦截,出现错误时可以实现降级策略. 比如跳转页面不存在不是直接crash而是可以跳转到一个指定的默认页面。
四、用ARouter进行模块化开发
接下来,将会用一个demo介绍如何用ARouter进行模块化开发,demo模块化的整体架构如下:
- app:项目的宿主模块,仅仅是一个空壳,依赖于其他模块,成为项目架构的入口
- baselibrary:项目的基类库,每个子模块都依赖共享公用的类和资源,防止公用的功能在不同的模块中有多个实现方式
- module_route:集中管理所有模块的route
- module_main:闪屏页,登录页,主页等
- module_home:首页模块
- module_mine:我的模块
-
module_video:视频模块
module
五、依赖模式与独立运行模式切换
在项目开发中,各个模块可以同时开发,独立运行而不必依赖于宿主app,也就是每个module是一个独立的App,项目发布的时候依赖到宿主app中。各业务模块之间不允许存在相互依赖关系,但是需要依赖基类库。单一模块生成的apk体积也小,编译时间也快,开发效率会高很多,同时也可以独立测试。要实现这样的效果需要对项目做一些配置。
1、gradle.properties配置
在项目gradle.properties中需要设置一个开关,用来控制module的编译,如下:
isModule=false
当isModule为false作为依赖库,只能以宿主app启动项目,选择运行模块时其他module前都是红色的X,表示无法运行
依赖宿主模式
当isModule为true的时候作为单独的模块进行运行,选择其中一个module可以直接运行
独立运行模式
2、清单文件配置
module清单文件需要配置两个,一个作为独立项目的清单文件,一个作为库的清单文件,以module_main模块为例:
清单文件
buildApp作为依赖库的清单文件,和独立项目的清单文件buildModule区别是依赖库的清单文件Application中没有配置入口的Activity,其他都一样
3、gradle配置
gradle配置4、宿主app配置
宿主app配置六、ARouter功能详解
1、添加依赖和配置
android {
defaultConfig {
...
javaCompileOptions {
annotationProcessorOptions {
arguments = [moduleName: project.getName()]
}
}
}
}
dependencies {
// 替换成最新版本, 需要注意的是api
// 要与compiler匹配使用,均使用最新版可以保证兼容
compile 'com.alibaba:arouter-api:x.x.x'
annotationProcessor 'com.alibaba:arouter-compiler:x.x.x'
...
}
注意,arouter-api:1.3.1、arouter-compiler:1.1.4配置是
arguments = [moduleName: project.getName()]
arouter-api:1.4.1、arouter-compiler:1.2.2配置是
arguments = [AROUTER_MODULE_NAME: project.getName()]
2、添加注解
// 在支持路由的页面上添加注解(必选)
// 这里的路径需要注意的是至少需要有两级,/xx/xx
@Route(path = "/test/activity")
public class YourActivity extend Activity {
...
}
在我的demo中,各模块的route地址都统一放在module_route中集中管理,其他module都需要依赖module_route,同时不同模块的route有单独的RoutePath类。如图:
module_route
@Route(path = MainRoutePath.LOGIN_ACTIVITY)
public class LoginActivity extends BaseActivity {
...
}
public class MainRoutePath {
private static final String PREFIX = "/main/";
public static final String MAIN_ACTIVITY = PREFIX+"MainActivity";
public static final String LOGIN_ACTIVITY = PREFIX+"LoginActivity";
}
- 问题1:为什么要把route地址写在一个常量类里?
//声明的地方
@Route(path = "/test/activity")
//使用的地方
ARouter.getInstance().build("/test/activity").navigation();
声明的地方和使用的地方可以是处于不同的module,这种写法各module不需要相互依赖,貌似很好,耦合度很低。但这只是表面上看代码没有了耦合度,但他们的耦合关系还在,试想一下,声明的地方哪天把route地址改了,使用的地方完全“无感”,只有等到真正运行时才能发现出错了,这种写法风险很大,而且不容易提前发现。如果声明的地方和使用的地方route都用一个常量来表示,就能很好的避免这种风险。
- 问题2:为什么不同模块的route有单独的RoutePath类
因为一个App一般页面都比较多,如果所有route都用一个RoutePath类来装,那这个类将会很大,且不同模块开发人员都需要去改这个类,容易产生混乱。如果不同模块的route有单独的RoutePath类,不同模块的开发人员只去改对应的类,代码会更好管理。 - 问题3:为什么要单独建一个module_route,仅仅只配置route,为什么不把RoutePath类放在baselibrary里?
baselibrary一般是一些通用的基础功能或通用配置,正常情况下应只能让少数的有架构层次的开发人员去改动,所以应该做权限保护,如果把RoutePath放在baselibrary里,相当于baselibrary对所有开发人员都是开发的。 - 问题4:配置route地址时,有什么讲究?
如上MainRoutePath中,我的route地址配置规则采用的是: 前缀 + Activity类名,前缀一般用module名字。根据官方文档说明
1、SDK中针对所有的路径(/test/1 /test/2)进行分组,分组只有在分组中的某一个路径第一次被访问的时候,该分组才会被初始化;
2、可以通过 @Route 注解主动指定分组,否则使用路径中第一段字符串(/*/)作为分组。
意思是用分组可以按需加载,提高性能,当没有主动分组时,ARouter用第一段字符串作为分组。所以我的前缀就是分组名,不用再去主动指定分组。
3、初始化SDK
if (isDebug()) { // 这两行必须写在init之前,否则这些配置在init过程中将无效
ARouter.openLog(); // 打印日志
ARouter.openDebug(); // 开启调试模式(如果在InstantRun模式下运行,必须开启调试模式!线上版本需要关闭,否则有安全风险)
}
ARouter.init(mApplication); // 尽可能早,推荐在Application中初始化
4、发起路由操作
// 1. 简单的跳转
ARouter.getInstance().build(MainRoutePath.MAIN_ACTIVITY).navigation();
// 2. 跳转并携带参数
ARouter.getInstance().build(MainRoutePath.MAIN_ACTIVITY)
.withString("name", name)
.withInt("age", 28)
.navigation();
5、目标页面接收参数
@Route(path = MainRoutePath.MAIN_ACTIVITY)
public class MainActivity extends BaseActivity {
/**
* 接收参数
*/
@Autowired(name = "name")
public String name;
@Autowired(name = "age")
public int age;
...
}
6、声明拦截器(拦截跳转过程,面向切面编程)
拦截都是全局性的,因此一般写在baselibrary里,如权限校验的拦截器。拦截器会在跳转之间执行,多个拦截器会按优先级顺序依次执行
AuthInterceptor
但是需要注意的是,每次所有的跳转都会执行拦截器操作,ARouter提供了greenChannel()方法进行跳转过去一切拦截器,在不需要拦截器的地方跳转的时候加上即可。
//greenChannel表示跳过拦截器验证
ARouter.getInstance().build(MainRoutePath.LOGIN_ACTIVITY).greenChannel().navigation();
7、降级策略
ARouter提供的降级策略主要有两种方式,一种是通过回调的方式;一种是提供服务接口的方式。我们分别来看看两种方式的使用方法:
- 一、单独降级-回调的方式
这种方式在跳转失败的时候会回调NavCallback接口的onLost方法。
ARouter.getInstance().build(MainRoutePath.MAIN_ACTIVITY).navigation(this, new NavCallback() {
@Override
public void onFound(Postcard postcard) {
Log.d("ARouter", "找到了");
}
@Override
public void onLost(Postcard postcard) {
Log.d("ARouter", "找不到了");
}
@Override
public void onArrival(Postcard postcard) {
Log.d("ARouter", "跳转完了");
}
@Override
public void onInterrupt(Postcard postcard) {
Log.d("ARouter", "被拦截了");
}
});
回调接口,对于降级策略主要实现感兴趣的onLost方法即可。
- 二、全局降级-服务接口的方式
这种方式很简单,主要处理逻辑在内部,暴露的接口很友好。
//跳转目标页面不存在,触发降级策略 避免crash
ARouter.getInstance().build("/test/test").navigation();
这种降级策略主要是实现服务接口DegradeService,就一个方法就是onLost,和上面的类似。
//要用ARouter跳转才能拦截到,用Intent隐式或显示跳转无法拦截,出错还是会crash
@Route(path = RoutePath.DEGRADE)
public class DegradeServiceImpl implements DegradeService {
@Override
public void onLost(Context context, Postcard postcard) {
ARouter.getInstance().build(RoutePath.DEGRADE_TIP).greenChannel().navigation();
}
@Override
public void init(Context context) {
}
}
全局降级-服务接口也应该写在baselibrary里
8、使用 IDE 插件导航到目标类
在 Android Studio 插件市场中搜索 ARouter Helper
, 或者直接下载文档上方 最新版本
中列出的 arouter-idea-plugin
zip 安装包手动安装,安装后 插件无任何设置,可以在跳转代码的行首找到一个图标 点击该图标,即可跳转到标识了代码中路径的目标类,如图:
ARouter Helper安装二
跳转快捷 图标
9、生成路由文档
// 更新 build.gradle, 添加参数 AROUTER_GENERATE_DOC = enable
// 生成的文档路径 : build/generated/source/apt/(debug or release)/com/alibaba/android/arouter/docs/arouter-map-of-${moduleName}.json
arguments = [AROUTER_MODULE_NAME: project.getName(), AROUTER_GENERATE_DOC: "enable"]
路由文档
七、Android Butterknife在library组件化模块中的使用问题
1、问题
当项目中有多module时,在使用Butterknife的时候会发现在library模块中使用会出问题。当library模块中的页面通过butterknife找id的时候,就会报错,提示@BindView的属性必须是一个常数,也就是说library module编译的时候,R文件中所有的数据并没有被加上final,也就是R文件中的数据并非常量。
2、解决步骤
- I 首先在项目的总build.gradle中添加classpath
classpath 'com.jakewharton:butterknife-gradle-plugin:8.2.1'
build.gradle
- II 在library中build.gradle中引入插件
apply plugin: 'com.jakewharton.butterknife'
library build.gradle
- III 在library中build.gradle中dependencies添加依赖
compile "com.jakewharton:butterknife:8.5.1"
annotationProcessor "com.jakewharton:butterknife-compiler:8.5.1"
3、butterknife在library activity中的使用和注意事项
1、用R2代替R findviewid
@BindView(R2.id.textView)
TextView textView;
@BindView(R2.id.button1)
Button button1;
@BindView(R2.id.image)
ImageView image;
2、在click方法中同样使用R2,但是找id的时候使用R
@OnClick({R2.id.textView, R2.id.button1, R2.id.button2, R2.id.button3, R2.id.image})
public void onViewClicked(View view) {
switch (view.getId()) {
case R.id.textView:
break;
case R.id.button1:
break;
case R.id.image:
break;
}
}
3、特别注意library中switch-case的使用,在library中是不能使用switch- case 找id的,解决方法就是用if-else代替
@OnClick({R2.id.textView, R2.id.button1, R2.id.button2, R2.id.button3, R2.id.image})
public void onViewClicked(View view) {
int i = view.getId();
if (i == R.id.textView) {
} else if (i == R.id.button1) {
} else if (i == R.id.image) {
}
}
八、 Demo地址
- ARouter的其他详细功能,可阅读官方文档:https://github.com/alibaba/ARouter/blob/master/README_CN.md
- 最后附上我的Demo地址:https://github.com/alanchenyan/AppModuleDemo