app组件化实战解析

2020-12-15  本文已影响0人  暴走的小青春

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采用发送和接收,需要一个约束去定义此

上一篇下一篇

猜你喜欢

热点阅读