上篇文章 Android 组件化 之 如何优雅的实现同级组件的通信 讲到
写代码这么多年,一个重要感受是「不要过度封装!」
不仅仅是说业务组件不多,没必要用这么复杂的组件化方案。
我甚至觉得组件化都不是必须的。
组件化的3条好处
- 一个工程(project)里面需要选择依赖哪几个组件,然后打成不同的包。
- 由第一条引申,如果有需求「每个组件可以单独打个包来测试」。
- 加快了增量编译的速度。
第一条「打成不同的包」是很广义的,除了依赖这一个工程打出不同包。还有比如有一些base组件,能自己项目里用,还能打出一个独立的aar供其他项目用,也算作打成不同的包。这些能抽离出来的base代码就可以作为一个组件。
组件化的牺牲
1. 如果分包能解决,为什么要牺牲便利性,抽离出组件来。
很多文章说组件化可以 「解耦」、「业务分层」、「业务隔离」、「代码变的更好维护」,这些其实都是可以通过分包(Package)解决的。
约定好 A小姐 负责 com.xxx.packageA 下的代码,B小姐 负责 com.xxx.packageB 的代码。甚至可以写个脚本,git clone 下载下来工程后,立刻执行 chmod -r 删去某些路径的写权限,避免无意修改别人负责的代码区域。
抽离出组件后并不是一蹴而就的事情,要一直投入精力保持组件成功运行也是需要不少成本的。
比如说我们的资源文件,一般会要求加模块名字的前缀,避免组件化项目运行过程中资源冲突。(谁让resourcePrefix它只管检验不帮忙动手啊)
实战中某个业务module里的UI或是什么代码,需要变成通用的,供多个业务模块使用,要下沉到 lib_base 模块了。这就需要转移各种资源到 lib_base,这就是要逐个文件、逐个资源重命名。还要牵扯到所有用到这个资源的代码,改一个资源名就可能是10+个甚至几十+个文件的变动。IDE是很智能的都帮忙改了,但是自己提交代码前检查 & 别人code review审查平白无故又要多N个文件的工作量,这种痛苦我深有体会。
2. 关于组件独立运行
有一些项目改造成组件化,实现的效果是「每个业务组件 + App壳组件都可以单独打出一个App」,可以方便地单独测试某个业务。设想跟写单元测试代码很相似。
然而效果是测试某个业务内容后,集成到主app里能保证不出问题?不能,这时候又要跑一遍集成后的测试流程。后面甚至发现单独测试业务模块并不能省时间,干脆省略了。久而久之组件独立运行成为了摆设。
3. 关于加快了增量编译的速度
其实现在Android studio的instant run和越来越优化成熟的编译机制,甚至买部好点贵点的电脑,都可以解决这个问题了。而且如果组件化维护不当,到处都是 api 引入组件,那编译速度反而有可能延长。
场景案例
写了这么多组件化的坏话,我也不是说组件化一无是处。
而是我觉得,判断需不需要组件化的唯一一条要素是,当你需要使用这个工程打出不同的app的时候,才需要组件化。
这里我举一个组件化完美适用的场景。
这是一款小说阅读类App,最下面 lib_base 基础组件,有3个平级的业务组件 lib_user(用户)、lib_read(阅读)、lib_ad(广告),上面有3个可以打成不同 App 的组件。
- 国内版本:用户、阅读、广告都用到,所以三者都引用到,打出一个国内App。
- 国外版本:不允许有广告啊,会被下架啊。排除掉广告组件,引用用户和阅读组件,打出一个国外版本App。
- 为了推广,为了上架谷歌市场单本作品付费阅读,甚至可以没有用户模块。作品资源直接集成到apk里,1美元你直接买断这部作品了,无需联网无需登录离线即可阅读。只引入阅读组件,打出一个单部作品的App。
从这个案例可以看到,核心原则是「是否需要依赖不同的业务层组件,打成不同的包」。
以后再新增的功能,比如说 「社交」、「购物」,应不应该独立成组件,都应该按照这个原则来。是不是有的App需要这个组件,有的App不需要这个组件。
比如说 「登录功能」,需要独立封装成一个 登录组件 吗?
如果没有要打包一个没有登录功能的App,不需要设计单独的 「登录组件」。
如果你的项目 登录 是 必须 的功能,否则后续页面都打不开,就更不应该设计单独的 登录组件了。这样的登录功能,应该放 lib_base 里,属于公共能力的一部分,不应该跟业务组件平级。
现象与问题
然后讲讲最近正在进行的一个组件化重构的方案。App有3个重要组件,「抢单」、「做单」、「我的」。跟小说阅读类的App的「书城」、「书架」、「我的」很像。所以我拿这个小说阅读类App当案例继续说了。
重构前有这几个模块
- 书城:用于找书。
- 书架:自己收藏的,最近阅读的书。最重要的阅读功能也放到这个组件里。
- 我的:个人页面。
这样的项目结构看起来还是很清晰明了的。但是运行时间长了会发现有一个问题。
书城
和书架
,这两个组件很特殊,有很多功能都是重合的。比如说对于作品的介绍页,作品章节列表页面。这些功能因为共用,都慢慢转移到了 lib_base 里面,导致 lib_base 越来越臃肿。
并且这种功能只用于书城
和书架
,并不能用在我的
组件里。如果以后要添加任务
和社区
组件,也不会用到这些页面的功能。
解决
lib_base 越来越臃肿了怎么办?
按照我上篇文章 2.3 ARouter的服务管理 (更合适的接口定义) 讲的——定义好每个组件的能力,把「这个组件能提供的能力」封装成一个接口类放到base组件里。这是一种好方法,但是还有一些问题解决不了。
例如一些实体类(BookInfo、AuthorInfo),一些共用的资源文件,图片、UI布局、shape、文案,还是要放到 lib_base里。因为服务管理
不能提供实体类和资源文件的共用。
放 lib_base 最不好的一点是,这些资源只有书城
和书架
两个组件用到,我的
组件不会用到,它们不能成为 通用资源
。
对于这种项目,某部分组件依赖和共用很严重的,可以按照 2.4 ARouter的服务管理 (还能优化的空间) 来设计。
作品详细介绍页面
、作品章节列表
、作者详细介绍
这3个页面Activity放到业务组件里,可以使用ARouter跳转到。
而相关能力接口、实体Bean、resources等这些共用的资源,就可以放到export层里面了。
比如这里我们开发小组讨论协商,先划分一些功能归属。
module_书城_export
负责 作品详细介绍页面
、作者详细介绍
,所以 BookDetailEntity.kt、AuthorDetailEntity.kt 放到这个组件里。
module_书架_export
负责 作品章节列表
,所以 BookChapter.kt 放到这个组件里。
代码结构
1.Provider
使用ARouter的服务管理能力,在export组件创建一个Provider接口。在这里是 BookshelfProvider.kt,在这个案例里提供了的能力如上图。
一个重点是,这些能力可以是立即return返回回来,也可以通过设立回调Callback,或者是利用rxjava的一个请求。
module_bookshelf_export 定义能力接口,module_bookshelf 写具体实现这些能力的方法。
如果一个接口过于杂乱,还可以分为多个。
举个例子创建一个provider包,有3个Interface文件,对应三个细分能力的分类
- BookHistoryProvider.kt 表示 书架Tab 里面的 历史阅读书籍 列表
- BookLocalProvider.kt 表示 书架Tab 里面的 手机本地书籍 列表
- BookSubscriptionProvider.kt 表示 书架Tab 里面的 用户订阅书籍 列表
2.实体
比如 getBookShelfList 获取书架的所有作品列表 这个接口,不可能返回字符串让其他组件解析吧。所以相关的共用的实体类也应该定义在这里。
3.资源文件
各类资源文件,包括自定义View,这类都是Provider无法提供的能力,也应该定义在这里。
总结
- 判断某块功能需不需要独立成组件?判断标准是 「是否需要依赖不同的业务层组件,打成不同的包」
- 代码应该放在哪里:
区分:哪些是所有业务组件都需要的能力,哪些是一部分业务组件需要的能力,哪些是只有自己用的能力。
使用范围 | 放置位置 |
---|---|
只有自己使用 | module_业务 |
需要给个别业务组件使用 | module_业务_export |
全部业务组件都使用 | lib_base |