依赖版本管理:版本目录详解、版本冲突解决、强制版本、依赖约束、平台依赖

依赖版本管理,说白了就是管好你项目里那些第三方库的版本号。我见过太多项目,因为版本号写得到处都是,升级一个库要改十几个文件,最后干脆放弃治疗。嗯,今天我们就来彻底解决这个问题。

版本目录(Version Catalog)—— 集中管理版本号

Gradle 7.0 之后引入了版本目录,我个人觉得这是近年来最好的改进之一。以前我们习惯在 ext 块里定义版本号,但那种方式不够结构化,IDE 支持也差。

版本目录的核心思想:把所有依赖的坐标和版本号,统一放到 libs.versions.toml 文件里管理。

libs.versions.toml 文件结构

[versions]
kotlin = "1.9.0"
compose-bom = "2024.02.00"
retrofit = "2.9.0"

[libraries]
kotlin-stdlib = { module = "org.jetbrains.kotlin:kotlin-stdlib", version.ref = "kotlin" }
compose-bom = { module = "androidx.compose:compose-bom", version.ref = "compose-bom" }
retrofit = { module = "com.squareup.retrofit2:retrofit", version.ref = "retrofit" }

[bundles]
compose = ["compose-bom", "compose-ui", "compose-material3"]

[plugins]
kotlin-android = { id = "org.jetbrains.kotlin.android", version.ref = "kotlin" }

然后在 build.gradle.kts 里这样引用:

dependencies {
    implementation(libs.kotlin.stdlib)
    implementation(platform(libs.compose.bom))
    implementation(libs.bundles.compose)
}

你想想看,这样改版本号只需要改一个地方。我在项目中遇到过团队里有人直接在 dependencies 块里写死版本号,结果升级时漏了一个,编译报错查了半天。用了版本目录之后,这种问题再也没出现过。

小技巧:版本目录文件默认放在 gradle/ 目录下。如果你用多模块项目,所有模块共享同一个版本目录,爽得很。

版本冲突解决 —— Gradle 的自动仲裁机制

依赖冲突是 Android 开发者的老朋友了。比如你依赖了 A 库和 B 库,它们各自依赖了不同版本的 C 库,这时候 Gradle 怎么选?

Gradle 默认的策略是:选择最高版本。这听起来合理,但有时候会出问题。我曾经遇到过一个坑:A 库依赖了 OkHttp 3.12,B 库依赖了 OkHttp 4.0,Gradle 自动选了 4.0。结果 A 库内部用了 OkHttp 3.x 的 API,4.0 里被标记为废弃了,运行时直接崩溃。

怎么排查?用这个命令:

./gradlew :app:dependencies --configuration implementation

它会输出一棵依赖树,你能清楚看到每个库的版本是怎么来的。

注意:依赖树输出可能很长,建议用 --scan 参数生成 Build Scan 网页,可视化查看。

强制版本 —— 我说了算

有时候 Gradle 的自动仲裁结果不是你想要的。比如你明确知道某个版本有问题,想强制项目里所有地方都用另一个版本。这时候可以用 force

configurations.all {
    resolutionStrategy {
        force("com.squareup.okhttp3:okhttp:4.11.0")
    }
}

或者更细粒度地,在依赖声明里加:

implementation("com.squareup.okhttp3:okhttp:4.11.0") {
    force = true
}

我个人习惯尽量少用 force,因为它相当于「暴力覆盖」,可能会让某些库的预期行为失效。但有些场景确实绕不开,比如安全补丁升级。

依赖约束 —— 更优雅的版本控制

依赖约束是 Gradle 6.0 之后引入的,比 force 更温和。它告诉 Gradle:「我建议用这个版本,但如果其他库有更严格的版本要求,可以覆盖我。」

dependencies {
    constraints {
        implementation("com.squareup.okhttp3:okhttp:4.11.0") {
            because("修复了 CVE-2023-XXXXX 安全漏洞")
        }
    }
}

依赖约束的好处是:它不会像 force 那样强行覆盖,而是参与版本仲裁。如果另一个库要求必须用 4.10.0,Gradle 会选 4.11.0 和 4.10.0 中更高的那个——也就是 4.11.0。但如果那个库要求 strictly 4.10.0,那约束就失效了。

依赖约束 vs 强制版本

特性 依赖约束 强制版本
优先级 参与仲裁,可被覆盖 最高优先级,不可覆盖
适用场景 建议性版本升级 必须使用某个版本
风险 高,可能破坏其他库

平台依赖 —— BOM 的妙用

平台依赖(Platform Dependency)是解决「一组库必须用相同版本」问题的利器。最典型的例子就是 Android Compose 和 Spring Boot。

Compose 的 BOM(Bill of Materials)用法:

dependencies {
    implementation(platform("androidx.compose:compose-bom:2024.02.00"))
    implementation("androidx.compose.ui:ui")
    implementation("androidx.compose.material3:material3")
    // 不用写版本号,BOM 统一管理
}

你想想看,如果没有 BOM,Compose 的 ui、material3、animation 等十几个库的版本号你得一个一个写,升级时还得保证它们互相兼容。有了 BOM,一个版本号搞定所有。

自定义平台依赖也很简单:

dependencies {
    implementation(platform("com.example:my-bom:1.0.0"))
}

只要你的 BOM 里用 dependencyManagement 声明了版本号,其他库引用时就可以省略版本。

经验之谈:我建议每个 Android 项目都引入 Compose BOM,哪怕你还没用 Compose。因为 BOM 里还管理了 androidx.core:core-ktxandroidx.lifecycle 等基础库的版本,省心不少。

知识体系总览

下面这张图总结了本章的核心内容,你可以把它当作一个快速参考:

依赖版本管理知识体系 版本管理 版本目录 版本冲突解决 强制版本 依赖约束 平台依赖(BOM) libs.versions.toml 统一管理版本号 自动选最高版本 依赖树排查 resolutionStrategy force = true

这张图把五个核心概念串起来了。版本目录是基础,帮你管好版本号;版本冲突解决是日常必备技能;强制版本和依赖约束是进阶手段,一个暴力一个温和;平台依赖则是批量管理的最佳实践。

最后说一句:版本管理没有银弹。我见过有人把所有依赖都 force 到最新版,结果项目编译不过;也有人完全不管版本冲突,等到运行时崩溃才去查。我的建议是:先用版本目录打好基础,遇到冲突时优先用依赖约束,实在不行再用 force。BOM 能用的地方尽量用,省心省力。


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