5、依赖管理(Dependency Management)
依赖管理,说白了就是管好你项目里那些「外来户」。
我刚开始用 Gradle 那会儿,觉得不就是加几行 dependencies 嘛,有啥好学的?结果项目一复杂,依赖冲突、版本混乱、构建时间暴涨……嗯,那叫一个酸爽。后来我才明白,依赖管理的水,比你想象的要深。
5.1 implementation vs api vs compileOnly
这三个关键字,是 Gradle 3.0 之后引入的。它们决定了依赖的「可见范围」和「传递方式」。我见过不少项目还在用 compile,那都是老黄历了。
implementation
这是最常用的。它表示:这个依赖只在当前模块内部使用,不会暴露给外部。
dependencies {
implementation 'com.squareup.okhttp3:okhttp:4.9.3'
}
好处是什么?编译更快。因为 Gradle 知道这个依赖不会泄露出去,所以它只需要重新编译当前模块,不用连累依赖方。
api
api 会把依赖暴露给外部。也就是说,如果模块 A 依赖了模块 B,而 B 用了 api 引入了一个库,那么 A 也能直接使用这个库。
dependencies {
api 'com.google.android.material:material:1.6.1'
}
什么时候用?比如你写了一个公共 UI 库,里面用到了 Material 组件,那调用方自然也需要这些组件。这时候用 api 就合理。
compileOnly
这个关键字的意思是:编译时需要,但打包时不要。说白了就是「我只借用你的接口,但运行时我自己带」。常用于注解处理器、或者某些只在编译期生效的库。
dependencies {
compileOnly 'com.google.auto.value:auto-value-annotations:1.9'
}
我曾经在做一个插件化项目时,就用 compileOnly 引入了宿主工程的接口。这样编译能通过,但最终 APK 里不会重复打包。
| 关键字 | 编译时 | 运行时 | 是否暴露给外部 |
|---|---|---|---|
| implementation | ✅ | ✅ | ❌ |
| api | ✅ | ✅ | ✅ |
| compileOnly | ✅ | ❌ | ❌ |
5.2 依赖传递与排除
依赖传递,是 Gradle 的默认行为。你引入一个库,它自己也有依赖,那些依赖会自动拉进来。听起来很方便对吧?但问题也出在这里。
我记得有一次,项目里同时引入了两个网络库,结果它们各自依赖了不同版本的 OkHttp。构建时直接报错:Duplicate class。嗯,这就是依赖传递带来的麻烦。
如何排除传递依赖?
有两种方式:
- 在依赖声明中排除
- 全局配置排除
// 方式一:单个依赖排除
implementation('com.example:library:1.0.0') {
exclude group: 'com.squareup.okhttp3', module: 'okhttp'
}
// 方式二:全局排除
configurations.all {
exclude group: 'com.squareup.okhttp3', module: 'okhttp'
}
我个人更推荐方式一,因为它更精确。全局排除容易误伤,你想想看,万一其他库也需要这个依赖呢?
查看依赖树
Gradle 提供了查看依赖树的命令:
./gradlew :app:dependencies
输出结果会以树形结构展示所有依赖,包括传递依赖。你可以清楚地看到哪个库引入了什么版本。
5.3 动态版本与锁定
动态版本,听起来很酷对吧?用 + 号或者 latest.release 就能自动拉取最新版。但我劝你,慎用。
// 不推荐
implementation 'com.squareup.okhttp3:okhttp:4.+'
// 也不推荐
implementation 'com.squareup.okhttp3:okhttp:latest.release'
为什么?因为不可控。你今天构建没问题,明天可能就炸了。我在一个 CI 流水线上遇到过这种情况:周五晚上构建通过,周一早上构建失败,原因是某个库的补丁版本改了 API。嗯,那个周末我过得并不好。
依赖锁定
Gradle 提供了依赖锁定机制,可以固定所有依赖的版本。你只需要在 build.gradle 中启用:
dependencyLocking {
lockAllConfigurations()
}
然后运行:
./gradlew dependencies --write-locks
这会生成一个 gradle.lockfile 文件。以后每次构建都会检查这个文件,版本不对就报错。
5.4 本地依赖(aar/jar/module)
不是所有依赖都来自远程仓库。有时候你需要引入本地的 aar、jar,或者直接依赖本地模块。
本地 jar
implementation fileTree(dir: 'libs', include: ['*.jar'])
这个写法会把 libs 目录下所有 jar 都引入。我个人习惯把第三方 jar 放在 libs 目录,然后按功能分文件夹。
本地 aar
aar 的引入稍微麻烦一点。你需要先在 build.gradle 中声明仓库:
repositories {
flatDir {
dirs 'libs'
}
}
dependencies {
implementation(name: 'my-library', ext: 'aar')
}
或者你也可以把 aar 放到 libs 目录,然后用 fileTree 引入:
implementation fileTree(dir: 'libs', include: ['*.aar'])
本地 module
这是最灵活的方式。你可以在 settings.gradle 中引入本地模块:
include ':mylibrary'
project(':mylibrary').projectDir = new File('../MyLibrary')
然后在 build.gradle 中依赖:
implementation project(':mylibrary')
这样做的好处是,你可以直接修改模块代码,实时生效。我在做组件化改造时,就把所有业务模块都改成了本地 module 依赖。调试起来非常方便。
知识体系总览
下面这张图,是我对本章内容的总结。你可以把它当作一个「依赖管理速查表」。
依赖管理这件事,说难不难,说简单也不简单。关键在于理解每个工具的设计意图,以及它们在实际项目中的表现。嗯,希望你能从这一章里找到适合自己的方式。