Android组件化实践
最近做组件化的一点心得
什么是组件化
不同于插件化直接让项目的功能模块成为插件(apk)直接运行,组件化依然只有一个主工程(app),但是项目中的功能模块可以被单独编译并运行,开发过程中就可以将每个功能模块独立出来,分配给不同的人去开发。
优点
- 代码解耦
- 模块单独编译,减少编译等待时间
- 功能整体在单个模块中,方便功能移植
组件化带来的便利体现在这3个方面,需要拆分不同的模块,代码势必要进行解耦。模块拆分完成后,可以单独编译,相比以前编译整个项目,减少等待时间,同时也方便以后功能移植。
如何实现
组件化主要通过gradle来实现,module项目是否可以编译取决于module的build.gradle文件的apply plugin是library还是application,所以可以通过一个可以配置的标识,让gradle根据不同的标识来执行不同的构建方式。
来看一个具体的实现例子
- app 主应用module
- baselib 基本base库
- httplib 网络框架
- login 登录模块
- router 路由模块
组件的gradle配置
除了app,其他的4个module都在根目录下的gradle.properties中添加一个布尔类型的标志位。
baselibDebug = false
httpDebug = false
loginDebug = false
routerDebug = false
分别修改4个module根目录下的build.gradle文件,通过判断布尔值,选择是application还是library,以baselib为例
if(baselibDebug.toBoolean()){
apply plugin: 'com.android.application'
}else{
apply plugin: 'com.android.library'
}
由于设置了false这里会去执行library,但是library Module不能有applicationId,所以需要删除build.gradle中的defaultConfig节点下的applicationId
defaultConfig {
applicationId "com.haibuzou.baselib" //删除
minSdkVersion 16
targetSdkVersion 25
versionCode 1
versionName "1.0"
testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner"
}
baselib已经变成了一个库工程,baselib中的manifest.xml是作为application的manifest,现在baselib变成了library就需要新的manifest.xml。根据不同条件使用不同的manifest.xml文件,通过配置sourceSets来完成
sourceSets{
main{
if(baselibDebug.toBoolean()) {
manifest.srcFile('src/main/debug/AndroidManifest.xml')
}else{
manifest.srcFile('src/main/release/AndroidManifest.xml')
}
}
}
准备2个manifest.xml
- 作为library工程的manifest需要删除测试用声明的activity。
- theme和icon的配置也需要删除避免manifest merge的时候重复声明导致冲突
- debug中的manifest可以做任何修改
app的gradle配置
组件在测试运行时会变成application无法被依赖,所以需要配置app不去依赖测试组件,同样还是在根目录下的gradle.properties中添加布尔值标志位
appDebug = false
修改app下的gradle中的依赖
if(appDebug.toBoolean()){
compile project('router')
}else{
compile project(':login')
}
httplib,baselib,router都会与login依赖所以只依赖了login模块,appDebug为true时选择去依赖router而不是baselib,为什么会这么做,后文会讲到
到目前为止模块的配置已经完成,如果需要运行组件,只需要将appDebug设置为true,测试的组件的xxxDebug设置为true,然后点一下gradle sync按钮就可以了,现在来看看组件化中碰到的问题。
问题
- module中对application代码引用
- 依赖包重复引用
- 不同模块之间activity的跳转
- 代码的解耦
module中对application代码引用
我在开发中会写一些方法或者声明静态全局变量在application中,方便全局的调用,最典型的比如声明静态baseContext指向application本身,后续开发会使用这个baseContext作为全局的Context来使用。
我的解决办法是在baselib中创建application,将原来app项目的application中的代码迁移到baselib的application中,app项目的application直接继承baselib的application。因为baselib作为基本库,其他的组件都会与baselib有依赖关系,组件在测试运行时就可以直接将baselib的application声明在debug的Manifest.xml中,组件测试运行可以引用之前app项目中的application的代码,组件变为library时也可以直接引用baseApplication中的代码,具体的代码在最后的例子项目中这里就不细说了。
依赖包重复引用
不同的组件依赖很容易重复依赖或者产生冲突,并且根本无法管理,我用的是比较普遍的做法, 将一个组件作为基本组件,gradle中做所有的依赖包操作,其他的组件都与这个基本组件建立依赖关系。本文的例子中我选择router作为基本组件。
不同module之间activity 跳转
组件化后activity分别在不同的module中,显示跳转直接获取activity的class这条路就不要想了。隐式跳转跳转是个办法,但是管理起来有点麻烦。所以我还是用的显示跳转,使用反射来获取activity的class属性来执行跳转,给大家分享一个我项目中的一个路由类的写法
/**
* Created by haibuzou on 2017/4/1.
* 路由跳转管理类
*/
public class RounterManager {
private static Map<String,Integer> rounterMap =new HashMap<>();
private static final int LOGIN = 1;
private static String HTTP_SCHEME = "http";
static {
rounterMap.put("login", LOGIN);
}
public static void rounter(String uriStr,Activity activity){
if (TextUtils.isEmpty(uriStr)) {
return;
}
try {
Uri uri = Uri.parse(uriStr);
String scheme = uri.getScheme();
String host = uri.getHost();
//判断当前是scheme是http web链接执行web跳转
if (scheme.contains(HTTP_SCHEME)) {
Class webActivity = Class.forName("com.rnhighfinance.ui.HywinWebActivity");
Method goToPage = webActivity.getDeclaredMethod("goToPage", Context.class, String.class);
goToPage.invoke(webActivity.newInstance(), activity, uriStr);
return;
}
//scheme不是http 判断host 决定跳转到哪个activity
int rounterType = rounterMap.get(host);
switch (rounterType) {
case LOGIN:
activity.startActivity(new Intent(activity, Class.forName("com.hywin.loginlib.login.LoginActivity")));
break;
}
}catch (Exception e){
e.printStackTrace();
}
}
}
路由的设计思路类似于web链接跳转,比如http://www.baidu.com会跳转到百度,uri的链接有2个主要的组成部分:scheme和host
- scheme代表协议比如http,ftp等等
- host代表主机地址比如www.baidu.com
RounterManager首先通过传入的uri链接获取scheme和host
String scheme = uri.getScheme();
String host = uri.getHost();
判断scheme包含http执行webActivity跳转。通过反射执行webActivity的goToPage方法,完成跳转。
if (scheme.contains(HTTP_SCHEME)) {
Class webActivity = Class.forName("com.rnhighfinance.ui.HywinWebActivity");
Method goToPage = webActivity.getDeclaredMethod("goToPage", Context.class, String.class);
goToPage.invoke(webActivity.newInstance(), activity, uriStr);
return;
}
如果scheme不包含http,会通过host匹配到login,执行跳转到LoginActivity
int rounterType = rounterMap.get(host);
switch (rounterType) {
case LOGIN:
activity.startActivity(new Intent(activity, Class.forName("com.hywin.loginlib.login.LoginActivity")));
break;
}
代码的解耦
代码的解耦其实兼具苦力活和技术活,因为需要迁移代码,所以会有很多复制粘贴的苦力活。同时还需要处理错综复杂的依赖关系,所以也是门技术活。如果依赖层次太复杂不好拨开的时候,个人建议使用反射去解耦,java的反射用法其实并不复杂。
获取class
Class test = Class.forName("包名")
通过class获取Method方法
方法的参数需要传入参数的class类型
Method method = test.getDeclaredMethod("方法名",String.class)
创建对象和获取method之后 调用方法
method.invoke(test.newInstance(),"hehe")
test.newInstance()方法用来创建对象,调用的是无参的构造方法
通过有参数的构造方法创建对象
Constructor constructor = test.getConstructor(String.class,int.class)
constructor.newInstance("123",123)
简单的说就是通过Class获取Constructor,通过Constructor.newInstance()方法来创建对象