一次 Android 项目组件化

组件化?

模块化

曾经也许你做过这样的事,将一些公共的代码组成独立的 module 并编译为 library 供各个子模块或其他项目引用,通常利用这种方式实现项目模块化(小功能独立拆分)。

组件化

那么什么是组件化?概括为:组件化是基于可重用目的将项目按照具体业务需求进行拆分,并能将拆分得到的组件进行灵活重组,减小耦合(业务需求上的拆分)。

模块化与组件化

组件化和模块化,两者都是为了实现解耦/重用而拆分项目为多个模块;相对于模块化,组件化的拆分粒度更大。

移动应用中项目组件化的目的就是让若干业务 module 能够并行开发,每一个业务 module 都能生产与之对应的 library 与 App,最终能够根据具体的需求灵活的组织不同的业务 module 生产主 App。对于 Android 项目中组件化的概念,可以看看这个 PPT

为什么组件化?

  1. 公共代码/业务代码解耦
  2. 共享模块
  3. 组件 module 代码的控制/编写/修改能保证不相干组件 module 代码的稳定性(不需要修改其他组件的业务,避免产生相关干扰,还能提高测试效率)
  4. 减少具体业务 module 的编译时间,提高开发效率(Android 编译…)

Android 项目组件化

组件化结构

Android 中组件化主要是通过脚本来控制,接下来以 Android Studio 中 Gradle 脚本为例来实现简单的一个项目组件化。这里不会讲解如何对主体业务进行组件化的拆分,因为不同的业务在不同的需求下都能产生一套合适自身的拆分规则。

组件化首先要保证不同的业务 module 既能够单独的生产、调试,也能够灵活的作为一个业务分支组建到主 App 中。我们知道,通过控制 gradle 脚本中的apply plugin: 'com.android.xxx'可以实现 Android module 运行于不同的状态,常用做法是自定义一个变量去控制当前业务 module 所处的状态。脚本如下:

1
2
3
4
5
if (isDevModel.toBoolean()) {
apply plugin: 'com.android.application'
}else{
apply plugin: 'com.android.library'
}

其中isDevModel定义在主项目 gradle.properties 文件中(当然也可以定义在子业务 Modue 中)。对于基础组件,比如通用的工具类库,按照模块化思路作为一个 library 就行。

注意:在 Android 开发中当一个 module 需要有applicationlibrary形态时,它们分别对应的AndroidManifest.xml文件是不一样的。所以需要为当前 module 提供两套AndroidManifest.xml文件,再编写脚本根据当前 module 需要处于的形态自动去选择编译对应的文件,脚本如下:

1
2
3
4
5
6
7
8
9
10
11
12
android {
// ...
sourceSets {
main {
if (isDevModel.toBoolean()) {
manifest.srcFile 'src/main/debug/AndroidManifest.xml'
} else {
manifest.srcFile 'src/main/release/AndroidManifest.xml'
}
}
}
}

项目组件化架构图大致如下所示:

Android_to_modularity_architecture.png

项目组件化后的结构图大致如下所示:

android_modularity_project_tree.png

其中,app module 是主 module;businessmodulebusinessmodule2是业务 module,可以单独作为一个 Application 运行或作为主 module 的依赖组建到其中运行;baselibrary作为基础组件 library 供其他任何 module 依赖使用。

组件化中 Module 组建

Module 之间互相依赖以便于组建满足需求的 App。举一个例子:现在需要发布 release 版本的 App,需要将 businessmodule1 业务组件发布出去,此时只需要将isDevModel修改为false,并在主 moduleapp中依赖 businessmodule1 module即可。依赖脚本如下:

1
2
3
4
5
6
dependencies {
// ...
if (!isDevModel.toBoolean()){
releaseCompile project(':businessmodule1')
}
}

其中,businessmodule1module 又是依赖基础组件baselibrarymodule 的:

1
2
3
4
dependencies {
// ...
compile project(':baselibrary')
}

当开发者单独对businessmodulemodule 进行业务修改时,再将isDevModel修改为true,单独运行这一个 module 即可。

因为单个业务组件 module 的依赖相对不会太多,从而使得编译时间大大减少。

组件化遇到的问题以及一些小技巧

业务组件之间通信

不同的组件 module 之间 Activity 跳转

  • 类名跳转(看这篇文章介绍),不方便携带数据
  • 显示换隐式,定义 schema 跳转(外链跳转 App),可携带少量数据

Module 间通信也可采用事件总线进行

  • EventBus/Otto/自定义 RxBus

不同环境业务组件使用不同环境的基础组件(解决 CI 中涉及到的自动化问题)

举个例子,比如基础组件 module 是一个网络请求库,其中涉及到了测试环境和生产环境使用不同的代码,业务组件 module 也需要根据不同的情况区别使用该网络请求库生成的 library。

基础组件 module 中根据不同的环境使用不同的代码。这里我们为baselibrarymodule 的 debug 和 release 模式分别准备一套代码,整体 module 结构图如下所示:

android_modularity_lib_multi_varaint.png

如上图所示,debug 和 release 中都有一个包名和类名相同的Name类,但其中的代码是不一样的。具体可以查看源码。为了能够在 debug 和 release 模式下分别使用设置的代码,需要使用sourceSets控制代码的合并,如下:

1
2
3
4
5
6
7
8
9
10
11
android {
// ...
sourceSets {
debug {
java.srcDirs = ['src/main/java', 'src/debug/java']
}
release {
java.srcDirs = ['src/main/java', 'src/release/java']
}
}
}

此外在发布该 library 时,需要指定一些设置,如下:

1
2
3
4
5
6
7
8
android {
// ...
defaultConfig {
// ...
defaultPublishConfig 'release'
publishNonDefault true
}
}

说明:

  • defaultPublishConfig ‘release’,默认 library 只会生产 release 下的版本,此版本将会被所有项目使用,通过defaultPublishConfig可以控制默认生产哪个版本的库。
  • publishNonDefault true,默认情况下不能生产所有版本的 library,通过设置publishNonDefaulttrue,可以同时生产所有版本的 library。

业务组件 module 依赖不同的基础组件生产的 library,如下:

1
2
3
4
5
dependencies {
// ...
debugCompile project(path: ':baselibrary', configuration: "debug")
releaseCompile project(path: ':baselibrary', configuration: "release")
}

在使用 CI 时,通过这样的配置脚本解决了多个 APK 包依赖同一组件生产的不同的 library,最终得到我们需要的开发/测试/生产 APK 包。

其他问题

  1. 资源名称冲突,可以添加resourcePrefix解决。注意当添加了该属性后,所有资源名称必须用此 prefix 开头,否则作为 library 时会报错。
  2. Application 冲突,通过设置exclude将业务组件 module 对应的 Application 文件隔离。具体参照 ModularityDemo

总结

解耦、独立业务模块开发(减少编译时间),这些都应该是项目组件化的理由。同时,对一个既有项目实施组件化,考虑足够是最高效的方式。

源码

源码

参考

  1. http://tools.android.com/tech-docs/new-build-system/user-guide#TOC-Referencing-a-Library
  2. https://www.kymjs.com/code/2016/10/18/01/