刚接到Leader组件化任务的时候,内心是有疑惑的。目前项目中,各种业务交杂在一起,互相跳转、互相请求数据。分模块的过程必然是痛苦的,需要增加模块之间通的信协议。对于一个5,6人的团队来说,全都放在一个大模块中似乎也没啥不好,可以随心所欲地调用,节约思考框架合理性的时间。
但最终让我解除疑惑是团队扩张的长远考虑,未来团队扩张到8~10人时,在同一个模块中开发不同的业务就会很乱。代码量2倍,掌握的难度就会超过4倍,按业务分模块一定是未来的方向。
顾名思义,组件化能够让开发者只需专注自己开发的组件,独立运行自己的模块,节省编译时间,减少因别人的问题导致工作被打断的可能。
组件化的实现围绕下面几个点
1、子模块单独编译
2、sdk和第三方库的版本一致性
3、资源重复定义
4、模块之间页面跳转
5、模块之间数据传递
6、模块初始化处理
1、子模块如何单独编译
我们希望在开发模式下,能够单独调试自己的模块,编译成独立的apk。而在主程序发布时,成为一个library
嵌入主工程。
首先在子模块build.gradle
中定义常量,来标示模块目前是否处于开发模式
def isDebug = true
在子模块的build.gradle
中进行模式配置。debug
模式下编译成独立app,release模式下编译成library。
if (isDebug.toBoolean()) {
apply plugin: 'com.android.application'
} else {
apply plugin: 'com.android.library'
}
两种模式下模块AndroidManifest.xml
文件是有差别的。作为独立运行的app,有自己的Application
,要加Launcher
的入口intent
,作为library
不需要。这个问题很好解决,写两个不同的AndroidManifest.xml
即可,并在gradle中进行配置。
sourceSets {
main {
if (isDebug.toBoolean()) {
manifest.srcFile 'src/main/debug/AndroidManifest.xml'
} else {
manifest.srcFile 'src/main/AndroidManifest.xml'
}
}
}
2、sdk和第三方库的版本一致性
不同module
依赖sdk版本不一致,会因兼容性问题导致编译问题。
不同module
引用了同一个第三方库的不同版本,并且这个库没有做到向前兼容,就有可能出现方法找不到、参数不对应等问题。
所以有必要统一整个project
的依赖版本。
在最外层build.gradle
中定义的常量能被整个project
的build.gradle
文件引用,统一的版本定义可以放在这里。
ext {
android_compileSdkVersion = 25
android_buildToolsVersion = '25.0.2'
android_minSdkVersion = 21
android_targetSdkVersion = 25
lib_appcompat = 'com.android.support:appcompat-v7:25.1.1'
lib_picasso = 'com.squareup.picasso:picasso:2.5.2'
lib_gson = 'com.google.code.gson:gson:2.6.1'
}
3、资源的重复定义
说到资源的重复定义,笔者趟过坑,如果主工程和子模块中重复定义了同名的资源。
主工程中
<string name="daddy">爸爸</string>
子工程中
<string name="daddy">干爹</string>
虽然编译不会出错,但是最后子模块中用到daddy
的地方都会显示爸爸
。
编译时子模块
的资源会和主工程
合并到同一个类中,所以资源重名会有问题。
但是资源也要模块化呀,总不能在底层找个统一的地方都扔在里面,gradle
提供了一个解决方案来避免重复定义的问题。
resourcePrefix "a_"
强制模块中的资源名称带有a_
前缀,否则编译不过。
聊到这里,我们知道了如何使用gradle
独立编译子模块,以及如何处理分模块导致的一些问题。但是除了主工程统一调度外,模块与模块之间也需要互相调起和访问,所以需要协议去统一,这个协议是模块间共同定义与使用的,所以写在底层。
4、模块之间页面跳转
首先想到的就是配置uri
去匹配模块AndroidManifest.xml
中的intentFilter
来启动相应Activity
,这种方式是解耦的,但有缺点,要跳转其它模块,得先去看别的模块的AndroidManifest.xml
进行入口适配,还得研究具体Activity
中的传参设置,虽然代码依赖上解耦了,但是实现逻辑上没有解耦,忍不了。需要在底层创建一个路由协议,让使用者通过协议方便地调用。
用注解把需要的参数写在路由协议的接口中。下面是moduleA
提供给其它模块跳转moduleA
中页面的接口:
public interface RouterA {
@RouterUri("test://host_a")
public Intent getIntentActivityA(@RounterParam("name") String name, @RounterParam("age") int age,
@RounterParam("phones") Phones phones);//Phones是一个自定义类
}
其中@RouterUri
表示跳转改页面需要匹配的uri
,这个uri
最终会拿去和moduleA
中的AndroidManifest.xml
中对应activity
的intentFilter
去匹配。
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface RouterUri {
String value() default "";
}
@RounterParam
用来表示目标activity
需要的参数,最终会在目标activity
中进行解析。
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.PARAMETER)
public @interface RounterParam {
String value() default "";
}
为什么用注解的方式写接口而不是直接定义跳转方法呢?
用注解的方式,可以把参数更直观地展现在最醒目的方法声明中。而写成实现的方法,参数会被写在方法内部,定义起来不方便,而且要带上少量逻辑,不够简洁。参考retrofit
框架,也是用注解方式去实现,简洁、方便。
**为什么接口返回的是Intent,而不是直接进行页面跳转呢? **
因为我们的项目中,实现这个跳转可能是activity
,可能是fragment
,也可能startActivityForResult
需要带入一个自定义的requestCode
。所以为了灵活性,直接返回Intent
。
写好了接口,还需要将接口中的参数组装成一个可进行跳转的Intent
。使用Proxy
生成类动态代理这个接口。
public class RounterBus {
//静态map存储代理接口的实例
private static HashMap<Class, Object> sRounterMap = new HashMap<Class, Object>();
/**
* 得到动态代理路由接口的实例
*
* @param c 接口类
* @param <T>
* @return
*/
public static<T> T getRounter(Class<T> c) {
T rounter = (T) sRounterMap.get(c);
if (rounter == null) {
rounter = (T) Proxy.newProxyInstance(c.getClassLoader(), new Class[] { c }, new InvocationHandler() {
@Override public Object invoke(Object proxy, Method method, Object[] args)
throws Throwable {
//从方法注解的获取uri
RouterUri routerUri = method.getAnnotation(RouterUri.class);
if (routerUri == null || TextUtils.isEmpty(routerUri.value())) {
throw new IllegalArgumentException(
"invoke a rounter method, bug not assign a rounterUri");
}
Uri.Builder uriBuilder = Uri.parse(routerUri.value()).buildUpon();
//从参数值和参数注解,获取信息,拼入uri的query
Annotation[][] annotations = method.getParameterAnnotations();
if (annotations != null && annotations.length > 0) {
for (int i = 0, n = annotations.length; i < n; i++) {
Annotation[] typeAnnotation = annotations[i];
if (typeAnnotation == null || typeAnnotation.length == 0) {
throw new IllegalArgumentException("method " + method.getName() + ", args at " + i + " lack of annotion RouterUri");
}
boolean findAnnotaion = false;
for (Annotation a : typeAnnotation) {
if (a != null && (a.annotationType() == RounterParam.class)) {
uriBuilder.appendQueryParameter(((RounterParam) a).value(), GsonInstance.getInstance().toJson(args[i]));
findAnnotaion = true;
break;
}
}
if (!findAnnotaion) {
throw new IllegalArgumentException("method " + method.getName() + " args at " + i + ", lack of annotion RouterUri");
}
}
}
Context context = AppContext.get();
PackageManager pm = context.getPackageManager();
Uri uri = uriBuilder.build();
Intent intent = new Intent(Intent.ACTION_VIEW, uri);
//查询这个intent是否能被接收用来进行跳转
List<ResolveInfo> activities = pm.queryIntentActivities(intent, 0);
if (activities != null && !activities.isEmpty()) {
return intent;
} else {
if (BuildConfig.IS_DEBUG) {
Toast.makeText(context, "子模块作为独立程序启动时,跳不到其他模块哟", Toast.LENGTH_SHORT).show();
} else {
throw new IllegalArgumentException("can't resolve uri with " + uri.toString());
}
}
return null;
}
});
sRounterMap.put(c, rounter);
}
return rounter;
}
}
上面代码包装了一个路由总线,来获取并缓存路由接口的实例。
例如:moduleB需要调起 moduleA
中的ActivityA
Intent intent = RounterBus.getRounter(RouterA.class).getIntentActivityA("xixi", 1, new Phones());
if (intent != null) {
startActivityForResult(intent, 0);
}
5、模块之间的数据传递
每个module
在底层library
中用接口定义自己公开出来的方法
public interface FunctionA {
public String getData(String key);
}
由总线进行管理
public class FunctionBus {
/**
* 方法总线,缓存的map
*/
public static Map<Class, Object> sFunctionClassMap = new HashMap<>();
/**
* 设置接口的实现类
* @param o
*/
public static void setFunction(Object o) {
Class[] interfaces = o.getClass().getInterfaces();
for (Class c : interfaces) {
if (sFunctionClassMap.containsKey(c)) {
throw new IllegalStateException("duplicate set function:" + c.getName());
}
sFunctionClassMap.put(c, o);
}
}
/**
* 获取接口的实现类
* @param c
* @param <T>
* @return
*/
public static<T> T getFunction(Class<T> c) {
T f = (T) sFunctionClassMap.get(c);
if (f == null) {
Toast.makeText(AppContext.get(), "you have not register function:" + c.getName(), Toast.LENGTH_SHORT).show();
return null;
}
return f;
}
}
每个module
需要提前在方法总线中传入接口的实例(建议在Application
.onCreate
时),别的模块才能访问到该方法。
moduleB
访问moduleA
提供的getData
方法:
FunctionBus.getFunction(FunctionA.class).getData("hi")
6、application初始化
子模块作为application
时,有一些初始化的工作需要在Application.onCreate
时进行。而作为library
时,调不到这个onCreate
。所以自己写一个静态方法,供主工程的Application
调用。
public class ApplicationA extends Application {
@Override public void onCreate() {
super.onCreate();
//给底层library设置context
AppContext.init(getApplicationContext());
}
/**
* 作为library时需要初始化的内容
*/
public static void onCreateAsLibrary() {
//给FunctionBus传入接口的实例
FunctionBus.setFunction(new FunctionA() {
@Override public String getData(String key) {
return "xixi";
}
});
}
}
主工程的Application
onCreate
时记得初始化子模块。
public class MainApplication extends Application {
@Override public void onCreate() {
super.onCreate();
AppContext.init(getApplicationContext());
ApplicationA.onCreateAsLibrary();
ApplicationB.onCreateAsLibrary();
}
}
想调试A模块,but某些功能需要依赖B
这时只需要把B模块作为library
引入A。并且记得在B模块Application.onCreate
时初始化一下A模块。是不是很轻量级?常用的话在gradle中设置一个开关就更方便了。
def isDebugWithB = true
if (isDebugWithB.toBoolean()) {
compile project(':moduleB')
}
ApplicationB.onCreateAsLibrary();
总结
大学毕业刚开始工作时,当时的Leader就表达过一个观点,随着团队人越来越多,产出会越来越多,但是人均产出明显会减少。随着代码量的增多,代码的复杂性会呈指数上升,增加了新同学融入工作的难度。读完这篇文章我们知道,通过组件化,新同学只需关注少量的代码就能快速融入工作,开发中也能更专注于自己的功能,编译更快,bug更少。爽歪歪!
文中代码 github地址:https://github.com/RainbleNi/ModuleDivider