app组件化实战解析
1.背景
公司当前的app都处于一个app中,并且由于大量使用kotlin,编译速度本来相对于纯java的代码会慢,全量编译一次相当的痛苦,所以要把项目向组件化的方向改进,本文主要阐述晓黑板的组件化思路以及实现
2.为何要实现组件化
相对于插件化的不稳定,组件化可以说是很多公司争相去做的方案了,那先来分析下组件化的优点:
1.单独运行
每个模块能够各自独立,不依赖于其他的模块,能做到独立运行,单独运行能大幅度减少编译的时间,从而大幅度提高开发效率
2.解耦
对代码的解偶性大幅提高,每个模块相当于一个小的app,做到了低耦合,高内聚,开发起来只需要关注当前模块的代码
3.减少bug发生几率
3.减少bug几率,不知道大家又没有发现,有时候你改动一个activity的layout的布局,并修改了此activity相应的代码,但其实这个layout也被其他activity使用,而你在另一个activity没有改相应的配置,这样很容易造成意想不到的奔溃,影响开发效率
3.实践组件化遇到的问题及解决方案
1.关于router跳转的兼容性问题
经过对比分析各个组件化通讯的实现方案
具体分析看此篇文章 [分析arouter实战]
最终选取了arouter作为路由的通讯方案,但是衍生出几个我们要解决的问题
1.1 原有跳转方式
private final static String PARAMETER_KEY1 = "key1";�
private final static String PARAMETER_KEY2 = "key2";�
...
/**
* @param context 上下文
* @param par1 参数1
* @param par2 参数2(可选参数,缺损时写Optional.absent())
*/
public static void startActivity(Context context, Object par1,� Optional<Object> par2) {
Intent intent = new Intent(context, B_Activity.class);
intent.putExtra(PARAMETER_KEY1, par1);
intent.putExtra(PARAMETER_KEY2, par2);
context.startActivity(intent);
}
1.2 aroute的跳转方式
ARouter.getInstance().build("/test/activity1")
.withString("name", "老王")
.withInt("age", 18)
.withBoolean("boy", true)
.withLong("high", 180)
.withString("url", "https://a.b.c")
.withSerializable("ser", testSerializable)
.withParcelable("pac", testParcelable)
.withObject("obj", testObj)
.withObject("objList", objList)
.withObject("map", map)
.navigation();
1.3 兼容的方式
首先所有的page跳转都调用routeToPagerProvider,最后一个参数是可变参数,然后在相应模块接收相应的参数
/**
* 各个模块之间的action 默认不带返回值
*/
fun routeToPagerProvider(context: Context, path: String, vararg params: Any) {
ARouter.getInstance().build(path).navigation(context, object : NavCallback() {
override fun onArrival(postcard: Postcard?) {
}
override fun onFound(postcard: Postcard?) {
(postcard?.provider as PagerProvider).routerTo(context, params)
}
})
}
//相应模块接收的参数
@Route(path = PAGER_IMAGE)
class ImagePagerProvider : PagerProvider {
override fun init(context: Context) {
}
override fun routerTo(context: Context, array: Array<*>) {
var paramList = array.toMutableList()
var params1 = paramList.getOrElse(0) { "" } as MutableList<Image>?
var params2 = paramList.getOrElse(1) { 0 } as Int
startImagePager(context, params1, params2, params3, params4)
}
}
其实本质就是利用了arouter的provider进行的跳转,只是做了一层封装而已,那有人会问了,到底arouter里的provider的跳转方式和传统写法的跳转方式有何不同,两者效率如何?
详细见如下文章,相信读完此文章会带你了解arouter的本质
2.关于业务组件的拆分
2.1 关于common-module的定义
对于common-module的个人理解,应当是聚合了每个lib所封装的module + 基类 +工具类+每个上层module要用到的业务代码+要用到的公用的第三方的lib
2.1.1 每个lib所封装的module的含义:
比如一开始你用的图片加载框架是glide,你可能每个地方都这么写
Glide.with(context)
.load(url)
.apply(options)
.into(imageView);
当这样的代码越来越多时你肯定会建一个utils类去精简你的代码
比如这样
GlideUtils.loadImage(context,url,imageView)
当然有的时候为了对比图片加载的速度,加载时消耗的内存,可能用ab test的方案,可能在某个特定的情况下用到了别的图片加载框架,比如:fresco 这时候就需要一个lib去以接口的形式去封装,也就是以特定的lib(glide or fresco)封装成新的lib库的形式,以此封装好的lib库可能会上传每个公司自己的maven仓库,写法比如这样:
ImageLoader.getInstance().loadImager(context,url,imageView)
那就到此为止了么,当然不是!
此时可能在业务层有对拿到的url做相关业务的逻辑性转换,从而调用不同的ImageLoader的方法
可能这样
// 某个在utils类
public static void processImage(Context context, String imageUrl, ImageView view) {
String url = convertUrl(imageUrl);
ImageLoader.getInstance().loadImager(context,url,imageView)
}
//业务代码的转换
public static String convertUrl(String url) {
//url转换
}
比如有些需求可能对图片加载的url有个关于业务层代码的转化或者做一些逻辑性的操作
此时项目里两处可以加载图片的方式分别是你封装过的utils类和ImageLoader的方式
比较直接的办法就是common-module对上层提供一个用api的方式来引用新的module,对下层用implementation来引用你封装过的imageLoader库,也就起到了承上启下的作用,上层对于imageLoader库包括原生的加载苦并没有直接可调用的方式,只能调用common-moodule的方法来加载图片,起到了很好的隔离效果
当然这对于网络框架和图片框架采用这种形式的比较多
2.1.2 每个上层module要用到的业务代码的含义:
这里的代码可能会比较的杂,当然如果到后面common-module过于膨胀的话,你可以另外启用一个module来做此功能
2.13 其他
当然其他的,包括基类 +工具类+公用的第三方的lib 毫无疑问是放入common-module中的,这里再提下module中事件通信所要拿的model类,应该放哪里,个人认为,还是那句话,如果觉得过于膨胀,直接依赖另外一个module来做此功能
3 关于接口module通信的方案
3.1 一对一的分发策略
简单来说就是一个模块的action牵扯到另外一个module的代码,可能另外一个还需要携带另外一个module的返回值的情况,也就是所说的接口下沉,当然我们没有必要重复造轮子,arouter已经帮我们实现了,我们直接封装一层就可以了,这是对于action的封装,分别是有返回值和无返回值的
/**
* 各个模块之间的action 默认不带返回值
*/
fun routeToActionProvider(path: String, vararg params: Any) {
ARouter.getInstance().build(path).navigation(null, object : NavCallback() {
override fun onArrival(postcard: Postcard?) {
}
override fun onFound(postcard: Postcard?) {
(postcard?.provider as UnitActionProvider).routerTo(params)
}
})
}
/**
* 各个模块之间的action 需要带有返回值的provider
* type参数是需要返回的类型
*/
fun <T : Any?> routeToReturnActionProvider(path: String, type: Class<T>?, vararg params: Any): T? {
var postcard = ARouter.getInstance().build(path).navigation(null)
if (postcard != null) {
var result = (postcard as ActionProvider<T>)
return result.routerTo(params)
}
return null
}
//这里在相应module的代码
@Route(path = ACTION_LOGOUT)
class LogoutProvider : UnitActionProvider() {
override fun routerTo(array: Array<*>) {
// do logout代码
}
override fun init(context: Context) {}
}
3.2 一对多或是多对多的分发策略
1.比如一个module的action触发了多个模块的改变,也就是常说的消息总线的方案
关于消息总线的方案具体可以看这篇文章:事件总线的分析
最后我们选取用livedatabus来做消息总线,但其实鲜为人知的是,上述文章虽然很好提供了思路,但是有个大坑(上述文章没提到,不过在最新的提交里已解决)
具体产生的原因和解决方案可以看如下关于如下文章 :[livedatabus解析和注意事项]
4. 总结以及后续
本文简单阐述了组件化的优点,以及原有项目改造成组件化遇到的相关的问题,但还有几个小问题还需要去解决
1 事件接收和发送
现在在采用livedatabus时不用去关注注册和取消注册,虽然事件的event是定义在一个类中,但难免有人直接用自定义的key采用发送和接收,需要一个约束去定义此