多模块架构下的依赖治理:如何避免 Gradle 依赖地狱

4 阅读4分钟

随着项目业务的扩张,你把项目拆成了 appcorefeature-loginfeature-home 等十几个模块。你觉得架构清晰了,解耦了,心里美滋滋。

直到有一天,App 突然崩溃,报错 java.lang.NoSuchMethodError,或者 NoClassDefFoundError

排查了一下午,你发现:

  • core 模块依赖了 OkHttp 4.9.0
  • feature-login 模块偷偷依赖了 OkHttp 3.12.0
  • Gradle 在打包时“自作聪明”地合并了版本,导致运行时找不到方法。

这就是**“依赖地狱”(Dependency Hell)**。在多模块开发中,如果缺乏治理,build.gradle 文件很快就会变成一堆难以维护的“魔法数字”和冲突代码。

今天我们来聊聊,在 2026 年,如何优雅地治理 Android 依赖。

1. 放弃 ext,拥抱 Version Catalogs

以前我们喜欢在根目录的 build.gradle 里写个 ext 代码块来管理版本号。后来有人用 buildSrc,虽然支持自动补全,但会导致每次修改版本号都触发全量编译(太慢了!)。

现在,Google 官方推荐的标准是 Version Catalogs (libs.versions.toml)

它的优势在于:

  • 统一管理:所有版本号在一个 TOML 文件里,一目了然。
  • 性能好:修改版本号不会导致构建脚本重新编译。
  • 类型安全:在 Kotlin DSL 中可以直接点出来,不用背字符串。

实战配置 (gradle/libs.versions.toml):

[versions]
kotlin = "1.9.22"
retrofit = "2.9.0"
okhttp = "4.12.0"

[libraries]
retrofit = { module = "com.squareup.retrofit2:retrofit", version.ref = "retrofit" }
retrofit-gson = { module = "com.squareup.retrofit2:converter-gson", version.ref = "retrofit" }
okhttp = { module = "com.squareup.okhttp3:okhttp", version.ref = "okhttp" }
okhttp-logging = { module = "com.squareup.okhttp3:logging-interceptor", version.ref = "okhttp" }

[bundles]
networking = ["retrofit", "retrofit-gson", "okhttp", "okhttp-logging"]

在模块中使用:

// app/build.gradle.kts
dependencies {
    // 以前:implementation("com.squareup.retrofit2:retrofit:2.9.0")
    // 现在:
    implementation(libs.bundles.networking) 
}

看,连依赖一整套网络库都只需要一行代码,而且绝对不会出现 RetrofitOkHttp 版本不匹配的问题。

2. api vs implementation:守好你的大门

这是多模块开发中最容易被忽视的细节,也是构建速度变慢的罪魁祸首。

  • implementation (推荐) :依赖只对当前模块可见。如果 Library A 修改了依赖,只有 A 会重新编译,依赖 A 的 App 模块不需要重新编译。
  • api (慎用) :依赖会“泄漏”给上层模块。如果 Library Aapi 依赖了 Gson,那么依赖 A 的 App 模块也能直接用 Gson。一旦 Gson 版本变了,整条链路都要重新编译。

治理原则: 除非你的模块是一个“聚合层”或者你的接口返回值直接暴露了第三方库的对象,否则律使用 implementation。这能显著减少增量编译的时间。

3. 解决冲突的终极武器:BOM (Bill of Materials)

当你的项目引入了 Firebase、Jetpack Compose 或者 OkHttp 全家桶时,经常遇到不同子库版本不一致的问题。

这时候,BOM 就是救星。BOM 是一个特殊的依赖,它不包含代码,只包含“版本清单”。你只需要依赖 BOM,然后引入子库时不需要写版本号

dependencies {
    // 引入 OkHttp 的 BOM
    implementation(platform("com.squareup.okhttp3:okhttp-bom:4.12.0"))

    // 下面这些都不需要写版本号,BOM 会自动保证它们版本一致且兼容
    implementation("com.squareup.okhttp3:okhttp")
    implementation("com.squareup.okhttp3:logging-interceptor")
}

这样,你永远不用担心 logging-interceptor 的版本和 okhttp 主体版本打架了。

4. 强制统一版本(ResolutionStrategy)

有时候,第三方库 A 依赖了 Gson 2.8,第三方库 B 依赖了 Gson 2.10。虽然 Gradle 默认会选高的,但有时候你想强制所有模块都用同一个版本,防止意外。

在根目录的 build.gradle.kts 中,你可以加一把“全局锁”:

allprojects {
    configurations.all {
        resolutionStrategy {
            // 强制所有模块的 Gson 都使用 2.10.1
            force("com.google.code.gson:gson:2.10.1")
            
            // 或者,如果遇到冲突直接报错,强迫开发者去解决(适合严格的项目)
            // failOnVersionConflict()
        }
    }
}

5. 定期体检:Dependency Analysis Plugin

即使有了上面的手段,随着时间推移,项目里还是会堆积很多不再使用的垃圾依赖。

推荐一个神器插件:Dependency Analysis Gradle Plugin (by Tony Robalik)。

它能帮你找出:

  1. Unused dependencies:声明了但没用的依赖(删掉减体积!)。
  2. Transitive dependencies used:你用了某个库,但没显式声明(比如你用了 Retrofit 带来的 OkHttp,但没在 gradle 里写 OkHttp)。这很危险,一旦 Retrofit 升级去掉了 OkHttp,你代码就红了。
  3. Wrong scope:该用 implementation 的你用了 api

使用方法: 跑一下 ./gradlew buildHealth,它会生成一份详尽的 HTML 报告,教你如何精简 build.gradle

总结

依赖治理不仅仅是为了“编译通过”,更是为了项目的长治久安。

  1. 标准化:全线迁移到 Version Catalogs (TOML)
  2. 隔离化:严控 api 使用,默认为 implementation
  3. 一致化:善用 BOMresolutionStrategy 解决版本冲突。
  4. 自动化:定期用插件扫描“僵尸依赖”。

把这些做好了,你会发现你的 Android 项目就像打理好的花园一样,清爽、稳定、构建飞快。