随着技术越来越成熟,这两年,组件化开发与插件化开发的热度一度高涨。对于组件化,有的人也喜欢称之为模块化开发,我也比较喜欢称之为
模块化开发
。使用模块化开发也已经有一段时间了,特此总结一下模块化开发的心得,防止以后忘记。
什么是模块化开发
对于模块化开发的概念,有的人可能还不是很了解,通俗的来讲就是:
- 将项目中的具体功能模块,如登录、个人中心等,拆分成一个一个单独的
module
, - 将其中公用的功能抽离出来,形成一个个单独的公用
module
,方便复用 - 然后像引用一些
library
项目一样,将这些功能module
引入到一个统一的module
中,组成一个app
- 但是与这些
library
有区别的地方在于,这些被拆分成的module
也可以在需要的时候单独作为一个app
运行
为什么要使用模块化开发
- 随着项目越来越庞大,项目的体积也会越来越臃肿,每次更改一下代码,可能就要编译很长时间,效率得不到保证
- 一个项目可能有多个人员开发,大家在同一个项目中开发和修改代码,可能就会对其他人员的代码带来影响和冲突
针对以上两个问题,模块化开发,可以让每个人手中的项目大小得到有效控制,每个人可以更加专注于自己的任务,而不用花太多精力去关心和check别人的代码
模块化开发的两种模式
模块化开发目前有两种模式 (图是网上截的,太懒,不想画图 @_@
)
-
submodule: 即整个项目只有一个
project
,在project
中构建多个功能模块module
,每个人单独负责一个module
,
每个module
都有自己的git
仓库,非常直观。但是不好的地方就是,整个project
的git
分支会很复杂,且团队协作的时候,大家都是在project
的app
模块中做测试,可能就会发生冲突。且因为所有的module
在一个project
中,每个人都可以修改他人f负责的module
,不是很安全
-
multi-project: 即将每个功能模块都拆分成一个单独的
project
,一个project
中一般包含自己测试使用的app
模块和功能模块。这样,每个project
都拥有自己单独的git
仓库,在团队协作时,每个人都有自己的app
模块来做测试,不会影响到他人,也修改不了他人的module
比较安全,但是项目初期时,对于代码规范,命名规范都必须规定好,否则最后合并成app
时可能会有冲突。
以上两种模式都是比较常用的模式,各有优点,submodule
更直观,开发更快速,multi-project
更解耦,更安全。大家可以根据实际情况选择。(因为个人比较习惯于multi-project
,所以下面的有些讲解可能偏向于multi-project
)
模块化开发的设计
模块化开发大致分为以下几层:(网上一搜一堆,没什么大区别,主要是对每个层级的细分)
- 壳工程
- 业务组件层
- 公共组件层
(图是网上截的,太懒,不想画图@_@
,将就看吧,和讲的大致也差不多)
壳工程
顾名思义,壳工程
只是一个空的app module
,本身不含任何功能页面,不做任何业务逻辑处理,可随时替换。主要作用就是将其他业务组件
的依赖添加到自己身上,形成完整的app
业务组件层
- 业务组件层由多个业务组件组成,每个
业务组件
代表项目的一个具体功能模块,例如登录、个人中心等。 - 当业务组件中的功能需要在多个地方使用时,可以将其在细化拆分为
基础业务组件
和商务业务组件
。
例如大众点评中,有两个这样的模块酒店
和高端酒店
,这两个模块中都用到了对于酒店搜索
功能,但是两个模块又不完全相同,这时可以将酒店搜索
做成一个单独的基础业务组件
,在两个模块中都调用酒店搜索
组件,这两个模块便属于商务业务组件
公共组件层
对于 公共组件层,以前有同事喜欢直接做成一个公用的library
,比如做成一个名为lib_common
的module
,所有的 公用util、网络访问工具、第三方库如分享、支付等 都放在里面,然后被 业务组件 调用。在项目小的时候,这也无关紧要,但是如果项目越来越大,lib_common
也可能会更着变得越来越臃肿,这就又回到了起点,最后不得不花费精力重新将lib_common
拆分。所以个人觉得不能全都将 公用部分 集中在lib_common
中,仍需划分。
- 基础组件层: 存放稳定的复用代码,不需频繁更改,与项目几乎没有什么实际关联,作为底层存在,例如 通用的 BaseActivity、BaseDialog、通用网络访问封装框架、工具类等,具体是放在一个组件中还是拆分为多个组件,根据实际状况而定
- 第三方库组件层: 项目中难免会用到一些第三方库,如分享、第三方登录、支付等,这些第三方库可以独立成一个组件或者根据复杂程度分成多个组件方便复用
- 通用组件(Common): 针对项目实际情况,抽离出公用的代码例如工具类形成的组件。看起来可能与 基础组件 的作用相似,但实际上 Common组件 与 基础组件 是有区别的,基础组件 是稳定的,在个人看来,基础组件 可以不仅应用于一个项目,可以在多个项目中使用而不需要做变动(随着不断完善,以后很可能就成为了自己公司的项目框架
^_^
。而 Common组件 是更加针对项目的实际情况的,大都会依赖于 基础组件,并对里面一些功能做二次封装以适用于项目实际情况,不适合在其他项目中不做修改就使用,且会根据项目需求的改变,Common组件 也有可能做频繁修改
业务数据交互时的处理
场景
开发中遇到业务数据交互是有大概率遇到的事,比如说项目中有两个模块一个是景点
,一个是美食
,当你到达一个 景点 后,会给你 推荐美食,而 推荐美食 的数据肯定是从美食模块
而来。
在上述场景中,一般常见的实现就是,将获取推荐美食数据接口
下沉到一个 公用组件 中,在 公用组件 中实现接口,然后美食
与景点
模块都调用 公用组件 中的这个接口,获取数据。看似没什么大问题,其实还存在一些隐患。假如,项目中这种交互的行为存在更多怎么办?都将这些数据接口下沉到 公用组件 ,在这里面实现吗?这样但增加了 公用组件 的体积,而且是危险的。因为对于 公用组件 上文已经说过,很有可能会频繁变动,而变动可能来自不同的业务组件,那么可能会有多个不了解美食
模块的开发人员参与到 公用组件 的修改,这意味着修改人员是有权限修改这些交互数据的获取的,这是危险的。或者还有另一种常见的实现,就是数据获取,仍然放在美食
模块,景点
模块直接调用美食
模块中的代码,但是这增加了两者之间的耦合。
解决
在我看来,业务组件之间是相互独立,没有依赖的,每个成员专注于自己的业务,无需关注其他成员的工作,且同时对自己的业务拥有绝对的掌控权,而将业务中的核心即数据处理放在公用层,是个危险的行为。那么该如何解决呢?下面以ARouter
路由框架为例
- 利用路由框架在一个 公用组件 中注册服务(甚至你也可以单独创建一个 路由组件 做这件事),这里的服务就是
ARouter
的IProvider
,IProvider
是个interface
,本身不做具体实现,非常easy和安全
// 声明接口,其他组件通过接口来调用服务
public interface FoodService extends IProvider {
List<FoodBean> getFoodData(int locationCode);
}
- 依赖公用组件,在
美食
模块中实现FoodService
// 实现接口
@Route(path = "/service/food", name = "美食服务")
public class FoodServiceImpl implements FoodService {
@Override
public List<FoodBean> getFoodData(int locationCode) {
List<FoodBean> list=new ArrayList();
// 具体实现
...
return list;
}
@Override
public void init(Context context) {
}
}
- 依赖公用组件, 在
景点
模块中获取美食数据
// 申明服务
@Autowired(name = "/service/food")
FoodService service;
// 调用服务
List<FoodBean> foodList = service.getFoodData(101);
关于模块化时,组件模式的切换
在开发时,开发人员有时需要组件作为一个单独的app
运行来调试,有时需要将组件集成到壳app
中调试,所以需要经常在app 和 library
间切换。下面是为了应对这种情况,常见的处理方法
- 在
project
的gradle.properties
中定义一个标记
# true: 作为一个 app 单独运行
# false: 作为一个组件
isApp=false
- 在
module
的main
目录下新建文件夹manifest
,再在manifest
文件夹中新建app
和module
两个文件夹,存放两种模式下的AndroidManifest.xml
- 对于
module
的build.gradle
的配置
// 因为在 gradle.properties 定义了,所以这边可以直接获取
def isApp = isApp.toBoolean()
if (isApp) {
apply plugin: 'com.android.application'
} else {
apply plugin: 'com.android.library'
}
...省略
// 作为一个组件时,是没有 applicationId 的
if(isApp){
applicationId "com.xxxxx.xxxxxx"
}
...省略
sourceSets {
// 作为 app 和 组件时的 AndroidManifest.xml 是不同的
main {
if (isApp) {
manifest.srcFile 'src/main/manifest/app/AndroidManifest.xml'
} else {
manifest.srcFile 'src/main/manifest/module/AndroidManifest.xml'
}
}
}
小提议
- 无论是使用
submodule
还是multi-project
,引入组件时,尽量将组件打包成aar
,再引入,这样可以编译更快 - 开发前,团队先规定好代码规范与资源命名规范,因为在最终组件合并成app时,如果组件中有资源名一致的资源,会产生冲突
感谢以下文章
Android工程模块化平台的设计(整理优化版)
Android组件化(业务拆分)
美团猫眼android模块化实战-可能是最详细的模块化实战