模块化架构:多模块项目的拆分原则与Gradle配置

说实话,我第一次接手大型项目时,看到的是一个「超级模块」——所有代码堆在同一个app模块里,编译一次要喝杯咖啡。后来我花了三个月,硬是把那个项目拆成了二十多个模块。今天聊的这些经验,很多都是从那次重构里摔打出来的。

为什么要模块化?

你想想看,一个项目如果只有一个模块,会有什么问题?

  • 编译慢:改一行代码,整个项目重新编译
  • 耦合高:业务逻辑混在一起,改一个功能可能影响十个
  • 协作难:多人同时改同一个模块,合并冲突是家常便饭
  • 测试难:单元测试跑不起来,因为依赖太多

模块化说白了,就是把大项目拆成小项目。每个模块独立编译、独立测试、甚至可以独立发布。我在项目中遇到过最极端的情况——一个模块编译出问题,整个团队都得等着。拆完之后,谁出问题谁负责,效率翻倍。

模块拆分原则

拆模块不是拍脑袋的事。我见过有人按页面拆,结果拆出几十个模块,维护成本反而更高。这里分享几个我常用的原则:

1. 按职责分层

这是最基础的拆分方式。我个人习惯把项目分成三层:

  • 基础层(Foundation):网络库、图片加载、日志、工具类
  • 业务层(Business):用户模块、订单模块、支付模块
  • 应用层(App):壳工程,负责组装和启动

每一层只能依赖下层,不能反向依赖。这个规则一旦打破,模块化就失去了意义。

2. 按业务领域拆分

比如电商项目,可以拆成:

  • :feature:home — 首页
  • :feature:product — 商品详情
  • :feature:cart — 购物车
  • :feature:order — 订单
  • :feature:payment — 支付

每个feature模块内部可以有自己的ViewModel、Repository、UI。模块之间通过接口通信,不直接依赖实现类。

3. 按复用性拆分

有些代码会被多个模块用到,比如自定义的Button、通用的Dialog。这些应该抽成独立的UI组件模块。我曾经把一套通用的图表组件拆出来,后来三个项目都在用,省了不少事。

模块化架构图

下面这张图展示了我常用的模块化分层结构。注意箭头方向——依赖只能从上往下,不能反过来。

App 壳工程 依赖所有业务模块 :feature:home 首页模块 :feature:order 订单模块 :feature:pay 支付模块 其他业务 ... :core:network 网络层 :core:common 通用工具 :core:ui UI组件 第三方库(Retrofit / Glide / Hilt ...)

Gradle配置实战

模块拆好了,Gradle配置是关键。我见过很多团队模块拆得漂亮,但Gradle脚本写得一塌糊涂。这里分享几个核心配置技巧。

1. 统一版本管理

所有模块的依赖版本必须统一。我个人习惯用 libs.versions.toml 文件:

// gradle/libs.versions.toml
[versions]
kotlin = "1.9.0"
compose-bom = "2024.01.00"
retrofit = "2.9.0"
hilt = "2.48"

[libraries]
retrofit-core = { group = "com.squareup.retrofit2", name = "retrofit", version.ref = "retrofit" }
retrofit-gson = { group = "com.squareup.retrofit2", name = "converter-gson", version.ref = "retrofit" }
hilt-android = { group = "com.google.dagger", name = "hilt-android", version.ref = "hilt" }

[plugins]
android-application = { id = "com.android.application", version = "8.2.0" }
kotlin-android = { id = "org.jetbrains.kotlin.android", version.ref = "kotlin" }
hilt = { id = "com.google.dagger.hilt.android", version.ref = "hilt" }

然后在每个模块的 build.gradle.kts 里引用:

// :feature:order/build.gradle.kts
plugins {
    alias(libs.plugins.android.library)
    alias(libs.plugins.kotlin.android)
    alias(libs.plugins.hilt)
}

android {
    namespace = "com.example.feature.order"
    compileSdk = 34
}

dependencies {
    implementation(project(":core:network"))
    implementation(project(":core:common"))
    implementation(libs.retrofit.core)
    implementation(libs.hilt.android)
}
小技巧:用 libs.versions.toml 管理版本,升级依赖时只需要改一个地方。我曾经在一个项目里手动维护版本,结果Retrofit升级到2.9.0,有个模块还卡在2.7.1,排查了半天才发现。

2. 模块间依赖控制

业务模块之间不能直接依赖。比如 :feature:order 想调用 :feature:payment 的功能,怎么办?

我的做法是:定义一个 :core:router 模块,里面放接口和路由协议。业务模块实现这些接口,但只依赖 :core:router,不依赖具体实现。

// :core:router/src/main/java/.../PaymentRouter.kt
interface PaymentRouter {
    fun navigateToPayment(orderId: String)
}

// :feature:order 中调用
class OrderViewModel(
    private val paymentRouter: PaymentRouter
) {
    fun pay() {
        paymentRouter.navigateToPayment("order_123")
    }
}

// :feature:payment 中实现
class PaymentRouterImpl @Inject constructor(
    private val navController: NavController
) : PaymentRouter {
    override fun navigateToPayment(orderId: String) {
        navController.navigate("payment/$orderId")
    }
}

这样 :feature:order 只依赖 :core:router,不直接依赖 :feature:payment。模块之间彻底解耦。

3. 编译优化

模块多了,编译反而可能变慢——如果配置不当的话。这里有几个优化点:

  • 开启并行编译:在 gradle.properties 里加 org.gradle.parallel=true
  • 开启构建缓存org.gradle.caching=true
  • 按需配置:开发时只编译改动的模块,用 --configure-on-demand
注意:并行编译有时会引发依赖冲突。我曾经遇到过两个模块同时编译,结果A模块依赖B模块的新版本,但B模块还没编译完,导致编译失败。解决方案是明确模块间的依赖顺序,或者在CI上禁用并行编译。

避坑指南

模块化架构看着简单,实际落地时坑不少。我踩过的几个:

  • 过度拆分:一个工具类也拆成一个模块,结果模块数量超过50个,管理成本飙升。我的建议是:模块数量控制在10-20个之间,超过30个就要警惕了。
  • 循环依赖:A模块依赖B,B模块又依赖A。Gradle会报错,但有时候是间接依赖导致的,排查起来很头疼。我的做法是:用 gradle dependencies 命令定期检查依赖树。
  • 资源冲突:不同模块里定义了同名的资源文件(比如 ic_launcher.xml),编译时不会报错,但运行时可能加载错资源。解决方案是:每个模块的资源文件名加上模块前缀,比如 ic_launcher_order.xml

总结

模块化架构不是银弹,但它确实是大型项目的必经之路。拆得好,团队协作效率翻倍;拆得不好,反而增加维护成本。我的建议是:从业务边界出发,先粗拆再细拆,不要一上来就追求完美。

嗯,关于模块化就聊这么多。如果你在配置Gradle时遇到问题,欢迎交流。


公众号:蓝海资料掘金营,微信deep3321