4. 依赖管理:依赖声明方式、仓库配置、依赖传递与排除、动态版本与锁定、平台依赖BOM
依赖管理,说白了就是告诉 Gradle:「嘿,我的项目需要哪些库,你去帮我下载下来」。这事看起来简单,但坑不少。我刚开始用 Gradle 那会儿,就因为在依赖上栽过跟头——版本冲突、传递依赖乱入、构建时好时坏……嗯,今天咱们就把这些事彻底捋清楚。
4.1 依赖声明方式:你总得告诉 Gradle 要什么
在 build.gradle.kts 里声明依赖,最常用的就是 implementation。但 Gradle 其实提供了好几种配置,每种都有它的用途。
dependencies {
// 当前模块能用,对外隐藏
implementation("com.squareup.okhttp3:okhttp:4.12.0")
// 编译时用,运行时不要(比如注解处理器)
compileOnly("com.google.auto.service:auto-service-annotations:1.1.1")
// 运行时用,编译时不需要
runtimeOnly("ch.qos.logback:logback-classic:1.5.6")
// 测试专用
testImplementation("junit:junit:4.13.2")
androidTestImplementation("androidx.test.ext:junit:1.2.1")
// API 级别的依赖——会泄漏给消费者
api("androidx.appcompat:appcompat:1.7.0")
}
我个人习惯:能用 implementation 就不用 api。为什么呢?因为 api 会把依赖暴露给使用你模块的人,容易引发版本冲突。你想想看,你只是内部用了个 Gson,结果所有依赖你的人都得被迫处理 Gson 版本,多冤啊。
api。否则一律 implementation。
4.2 仓库配置:去哪下载这些库?
声明了依赖,Gradle 得知道去哪找。这就是仓库配置的事。通常我们在 settings.gradle.kts 或 build.gradle.kts 里配置。
// settings.gradle.kts
dependencyResolutionManagement {
repositoriesMode.set(RepositoriesMode.FAIL_ON_PROJECT_REPOS)
repositories {
google()
mavenCentral()
// 私有仓库
maven { url = uri("https://jitpack.io") }
maven { url = uri("https://maven.aliyun.com/repository/public") }
}
}
这里有个细节:repositoriesMode 设成 FAIL_ON_PROJECT_REPOS 后,子模块就不能自己加仓库了。我遇到过团队里有人偷偷在子模块加了个不稳定的仓库,结果 CI 构建时好时坏……排查了半天。所以,统一管理仓库是个好习惯。
google() 和 mavenCentral()。
4.3 依赖传递与排除:别让不相关的东西进来
依赖传递是 Gradle 的默认行为。你引入一个库,它自己依赖的库也会被拉进来。大部分时候这是好事,但有时候会出问题。
我曾经遇到过一个 case:项目里用了两个不同的图片加载库,它们都依赖了某个旧版本的 OkHttp,结果运行时崩溃了。排查下来,就是传递依赖搞的鬼。
怎么解决?用 exclude 排除掉不需要的传递依赖:
implementation("com.squareup.picasso:picasso:2.8") {
// 排除 Picasso 内部的 OkHttp
exclude(group = "com.squareup.okhttp3", module = "okhttp")
}
如果你想全局排除某个传递依赖,可以在 configurations 里配置:
configurations.all {
exclude(group = "com.squareup.okhttp3", module = "okhttp")
}
或者,你也可以用 transitive = false 彻底关闭传递依赖——但我不建议这么做,除非你很清楚自己在干什么。因为关闭后,你得手动把所有需要的依赖都列出来,太累了。
exclude 精准排除,不要一刀切关闭传递依赖。
4.4 动态版本与锁定:版本号到底该不该写死?
很多新手喜欢用 com.squareup.okhttp3:okhttp:4.+" 这样的动态版本。看起来省事,但风险极大。你想想看,今天构建和明天构建可能拉到的版本不一样,万一某个小版本引入了破坏性变更……嗯,线上崩溃等着你。
我个人习惯:所有依赖都写死具体版本号。但如果你确实想用动态版本,Gradle 也支持:
// 动态版本(不推荐用于生产)
implementation("com.squareup.okhttp3:okhttp:4.+")
// 或者用 latest.release
implementation("com.squareup.okhttp3:okhttp:latest.release")
那版本锁定是怎么回事?Gradle 提供了 gradle.lockfile 机制,可以把当前所有依赖的精确版本记录下来。这样即使你用了动态版本,构建时也会用锁定的版本,保证可复现。
启用版本锁定:
// settings.gradle.kts
dependencyResolutionManagement {
@Suppress("UnstableApiUsage")
lockMode.set(LockMode.STRICT)
}
然后运行 ./gradlew dependencies --write-locks 生成 lockfile。之后每次构建都会检查 lockfile,版本变了就会报错。这招在 CI/CD 里特别有用。
4.5 平台依赖 BOM:统一管理版本的好帮手
BOM(Bill of Materials)是 Gradle 5+ 引入的特性,说白了就是一个「版本清单」。你引入 BOM 后,再声明同一生态的依赖时,就不用写版本号了。
最典型的例子是 AndroidX 和 Firebase:
// 引入 AndroidX 的 BOM
implementation(platform("androidx.compose:compose-bom:2024.06.00"))
// 不用写版本号了
implementation("androidx.compose.ui:ui")
implementation("androidx.compose.material3:material3")
implementation("androidx.compose.ui:ui-tooling-preview")
BOM 的好处很明显:所有依赖版本由 BOM 统一管理,不会出现「A 依赖 1.0,B 依赖 1.1」这种不一致的情况。我参与的一个大项目,有 30 多个模块,用了 BOM 之后,版本冲突问题减少了 80% 以上。
你也可以自己定义 BOM:
// 在独立的 BOM 模块中
dependencies {
constraints {
api("com.squareup.okhttp3:okhttp:4.12.0")
api("com.google.code.gson:gson:2.10.1")
api("com.squareup.retrofit2:retrofit:2.9.0")
}
}
然后其他模块引入这个 BOM 即可:
implementation(platform(project(":my-bom")))
implementation("com.squareup.okhttp3:okhttp") // 版本由 BOM 提供
依赖管理这块,说白了就是「告诉 Gradle 要什么、去哪找、怎么处理冲突」。我见过太多项目因为依赖管理混乱导致构建失败或运行时崩溃。记住几个关键点:implementation 优先、仓库统一管理、精准排除传递依赖、版本锁定保平安、BOM 统一管版本。做到这些,你的构建脚本就稳了一大半。
公众号:蓝海资料掘金营,微信deep3321