10、模块化构建(Multi-Module):模块拆分原则、模块间通信(接口/依赖注入)、模块化构建优化、按需配置(on-demand configuration)
模块化构建,说白了就是把一个巨型工程拆成多个小模块。我刚开始接触 Android 时,项目就一个 app module,所有代码堆在一起。那感觉就像把所有衣服塞进一个衣柜——找件衬衫得翻半天。后来项目大了,编译一次喝杯咖啡都等不完。嗯,模块化就是解决这个问题的。
模块拆分原则:别为了拆而拆
拆模块不是拍脑袋决定的。我见过不少团队,一上来就把项目拆成十几个模块,结果依赖关系乱成一锅粥。我个人习惯遵循几个核心原则:
- 单一职责:每个模块只做一件事。比如网络请求模块就别塞图片加载的逻辑。
- 按业务域拆分:登录、支付、首页这些天然的业务边界,就是模块的天然边界。
- 按复用性拆分:如果一段代码被多个地方引用,那就该独立成模块。比如基础 UI 组件库。
- 按变更频率拆分:经常改动的代码和很少改动的代码分开。底层框架模块一年改不了几次,业务模块每周都在变。
核心原则:模块之间应该是单向依赖,避免循环依赖。一旦出现循环依赖,你的构建时间会爆炸,而且 Gradle 会直接报错。
我在项目中遇到过最典型的反例:有人把工具类全部塞进一个叫 common 的模块,结果这个模块依赖了所有业务模块。你想想看,这还叫 common 吗?这分明是个大杂烩。
模块间通信:接口与依赖注入
模块拆开了,怎么让它们说话?这是个大问题。直接引用另一个模块的类?那耦合度就上去了。我个人推荐两种方式:
1. 接口隔离
每个模块定义自己的接口,其他模块只依赖接口,不依赖实现。举个例子:
// 登录模块定义接口
interface LoginService {
fun isLoggedIn(): Boolean
fun getUserId(): String
}
// 支付模块只依赖接口,不依赖登录模块的具体实现
class PaymentManager(private val loginService: LoginService) {
fun pay() {
if (loginService.isLoggedIn()) {
// 执行支付
}
}
}
这样做的好处是,登录模块的实现可以随便改,只要接口不变,支付模块就不受影响。我曾经因为接口设计不合理,导致改一个接口,牵连了五个模块——那滋味真不好受。
2. 依赖注入(DI)
用 Dagger 或 Hilt 来管理模块间的依赖。说白了,就是让 DI 框架帮你把对象组装好,模块之间不需要知道对方的存在。
// 在 Hilt 中,模块提供依赖
@Module
@InstallIn(SingletonComponent::class)
object NetworkModule {
@Provides
fun provideOkHttpClient(): OkHttpClient {
return OkHttpClient.Builder().build()
}
}
// 其他模块直接注入
@Singleton
class UserRepository @Inject constructor(
private val okHttpClient: OkHttpClient
) {
// ...
}
我的建议:小项目用 Hilt 就够了,别上 Dagger。Hilt 的模板代码少很多,而且对新手友好。我见过有人在小项目里硬上 Dagger,结果配置写了一堆,实际收益几乎为零。
模块化构建优化:让编译快起来
模块化的一大好处就是增量编译。你改了一个模块,只需要重新编译那个模块和依赖它的模块。但前提是——你得配置对。
我踩过几个坑,分享给你:
- 避免模块间频繁的 API 变更:每次改接口,所有依赖模块都得重新编译。我建议接口稳定后再发布。
- 使用 Gradle 构建缓存:
org.gradle.caching=true这个配置一定要开。它能缓存编译结果,下次构建直接复用。 - 并行编译:
org.gradle.parallel=true。多核 CPU 不用白不用。 - 按需配置(on-demand configuration):这个我单独讲。
按需配置(on-demand configuration)
按需配置是 Gradle 4.6 引入的特性。它的原理很简单:只配置你需要构建的模块,其他模块跳过配置阶段。
你想想看,一个大型项目有几十个模块,每个模块的 build.gradle 都要执行一遍配置脚本。如果我只想改一个模块,其他模块的配置完全是浪费。按需配置就是解决这个问题的。
开启方式:
// gradle.properties
org.gradle.configureondemand=true
但要注意,这个特性不是银弹。我在项目中遇到过一个问题:某个模块在配置阶段依赖了另一个模块的产物,按需配置跳过了那个模块,结果编译失败。所以,如果你的模块配置脚本里有跨模块的依赖逻辑,按需配置可能会出问题。
避坑指南:我曾经在一个 30+ 模块的项目里开启按需配置,结果 CI 上频繁报错。后来发现是某个模块的 build.gradle 里直接引用了另一个模块的 BuildConfig 字段。解决方案是把这些跨模块的配置移到运行时,而不是配置时。
模块化架构总览
下面这张图展示了一个典型的多模块项目结构。我习惯把模块分为三层:基础层、业务层、应用层。
从图里能看出来,依赖方向是单向的:app 依赖业务模块,业务模块依赖基础模块。基础模块之间尽量不要互相依赖。这个结构我用了好几年,稳定性很高。
总结一下
模块化构建不是一蹴而就的。我建议你从一个小模块开始,比如把网络请求抽出来。跑通了再拆下一个。别想着一步到位,那只会让你陷入配置地狱。
最后说一句:模块化是为了让开发更高效,不是为了炫技。如果你的项目只有两三个开发,拆太多模块反而增加维护成本。量力而行。