14、构建脚本进阶:buildSrc vs 复合构建、版本目录、约定插件
说实话,Gradle 构建脚本写久了,你一定会遇到一个瓶颈:重复。
每个模块的 build.gradle.kts 里,依赖版本散落一地,插件配置反复粘贴,compileSdk、minSdk 这些基础参数每个 module 都要写一遍。我早期带团队时,项目才 8 个模块,但每次升级 AGP 版本,我都要改 8 个文件。改漏一个,编译就炸。那滋味,真不好受。
后来我花了整整一周,把项目重构了一遍,引入了今天要讲的这几个东西。效果立竿见影。说白了,这一章就是帮你把构建脚本从「手工作坊」升级到「工业化流水线」。
14.1 为什么需要进阶构建脚本?
先看一个典型痛点。假设你的项目里有三个模块:app、core、feature。每个模块的 build.gradle.kts 里都有这么一段:
android {
compileSdk = 34
defaultConfig {
minSdk = 24
targetSdk = 34
}
}
dependencies {
implementation("androidx.core:core-ktx:1.12.0")
implementation("androidx.lifecycle:lifecycle-runtime-ktx:2.7.0")
}
三个文件,三份拷贝。哪天 compileSdk 升到 35,你得改三处。漏改一处,编译警告就飘红。你想想看,这还只是三个模块,要是三十个呢?
所以我们需要一种机制:一处定义,处处生效。这就是 buildSrc、复合构建、版本目录和约定插件要解决的问题。
14.2 版本目录(Version Catalog)—— 依赖管理的终极方案
版本目录是 Gradle 7.0 引入的特性,官方叫法 Version Catalog。我个人觉得,这是 Gradle 团队近几年做的最正确的一件事。
以前我们管理依赖版本,要么在 ext 块里写一堆变量,要么用 buildSrc 里的 Kotlin 常量。这两种方式都有问题:ext 块没有类型安全,buildSrc 会导致整个项目重编译。版本目录完美解决了这两个痛点。
14.2.1 如何定义版本目录?
在项目根目录下创建 gradle/libs.versions.toml 文件。注意,文件名和路径是固定的,Gradle 会自动识别。
[versions]
agp = "8.2.2"
kotlin = "1.9.22"
core-ktx = "1.12.0"
lifecycle = "2.7.0"
[libraries]
core-ktx = { group = "androidx.core", name = "core-ktx", version.ref = "core-ktx" }
lifecycle-runtime = { group = "androidx.lifecycle", name = "lifecycle-runtime-ktx", version.ref = "lifecycle" }
[plugins]
android-application = { id = "com.android.application", version.ref = "agp" }
kotlin-android = { id = "org.jetbrains.kotlin.android", version.ref = "kotlin" }
然后在模块的 build.gradle.kts 里,你就可以这样引用:
plugins {
alias(libs.plugins.android.application)
alias(libs.plugins.kotlin.android)
}
android {
compileSdk = 34
defaultConfig {
minSdk = 24
targetSdk = 34
}
}
dependencies {
implementation(libs.core.ktx)
implementation(libs.lifecycle.runtime)
}
看到 libs.core.ktx 了吗?这是 Gradle 自动生成的类型安全访问器。点号对应 TOML 文件里的横线或点。比如 core-ktx 变成了 core.ktx。
14.2.2 版本目录的优势
- 类型安全:写错依赖名,IDE 直接报红,不用等到编译
- 集中管理:所有版本号在一个文件里,一目了然
- 支持多项目共享:可以发布版本目录到 Maven 仓库,供多个项目使用
- 不触发重编译:修改 TOML 文件不会导致整个项目重新编译,比 buildSrc 快得多
14.3 buildSrc vs 复合构建——两种共享代码的方式
版本目录解决了依赖版本的问题,但如果你还想共享插件配置、自定义任务、或者一些工具函数,就需要更强大的方案。这时候 buildSrc 和复合构建就登场了。
14.3.1 buildSrc 是什么?
buildSrc 是 Gradle 的一个特殊目录。放在 buildSrc/src/main/kotlin 下的代码,会自动编译并添加到项目的 classpath 中。所有模块的 build.gradle.kts 都可以直接使用。
举个例子,你可以在这里定义一个依赖常量:
// buildSrc/src/main/kotlin/Deps.kt
object Deps {
const val coreKtx = "androidx.core:core-ktx:1.12.0"
const val lifecycleRuntime = "androidx.lifecycle:lifecycle-runtime-ktx:2.7.0"
}
然后在模块里直接用:
dependencies {
implementation(Deps.coreKtx)
}
听起来不错对吧?但 buildSrc 有个致命缺点:任何修改都会导致整个项目重新编译。你改了一行 Kotlin 代码,整个项目的缓存就全废了。我早期用 buildSrc 时,每次改依赖版本都要等 2 分钟编译,项目大了之后简直要命。
14.3.2 复合构建(Composite Build)—— 更好的替代方案
复合构建是 Gradle 3.1 引入的特性。它允许你把一个独立的 Gradle 项目作为另一个项目的依赖。说白了,就是「项目里套项目」。
具体做法:
- 在项目根目录创建一个
build-logic目录(名字随意) - 在里面放一个完整的 Gradle 项目,包含 build.gradle.kts 和 settings.gradle.kts
- 在主项目的 settings.gradle.kts 里引入:
includeBuild("build-logic")
这样做的好处是:修改 build-logic 里的代码,不会触发主项目重编译。因为它们是两个独立的构建。只有当你更新了 build-logic 的输出(比如发布新版本的插件),主项目才会重新加载。
14.4 约定插件(Convention Plugin)—— 把重复配置打包成插件
约定插件,说白了就是把一堆重复的 android 配置、依赖声明、插件应用,打包成一个自定义插件。然后每个模块只需要 apply 这个插件,所有配置自动生效。
14.4.1 如何创建一个约定插件?
假设你有一个复合构建项目 build-logic,在里面创建约定插件:
// build-logic/convention/src/main/kotlin/AndroidApplicationConventionPlugin.kt
class AndroidApplicationConventionPlugin : Plugin<Project> {
override fun apply(target: Project) {
with(target) {
with(pluginManager) {
apply("com.android.application")
apply("org.jetbrains.kotlin.android")
}
extensions.configure<ApplicationExtension> {
compileSdk = 34
defaultConfig {
minSdk = 24
targetSdk = 34
versionCode = 1
versionName = "1.0"
}
buildTypes {
release {
isMinifyEnabled = true
proguardFiles(
getDefaultProguardFile("proguard-android-optimize.txt"),
"proguard-rules.pro"
)
}
}
}
dependencies {
"implementation"(platform("androidx.compose:compose-bom:2024.01.00"))
"implementation"("androidx.compose.ui:ui")
"implementation"("androidx.compose.material3:material3")
}
}
}
}
然后在 build-logic/convention/src/main/resources/META-INF/gradle-plugins 目录下创建属性文件,声明插件 ID:
# build-logic/convention/src/main/resources/META-INF/gradle-plugins/com.example.android-application.properties
implementation-class=AndroidApplicationConventionPlugin
最后,在 app 模块的 build.gradle.kts 里,只需要一行:
plugins {
id("com.example.android-application")
}
所有配置——compileSdk、minSdk、Compose 依赖——全部自动生效。干净利落。
14.5 知识体系总览
下面这张图帮你理清这几个概念的关系:
14.6 如何选择?一张表说清楚
| 方案 | 适用场景 | 性能影响 | 学习成本 | 我的推荐 |
|---|---|---|---|---|
| 版本目录 | 依赖版本管理 | 无(纯配置文件) | 低 | 所有项目必用 |
| buildSrc | 小型项目,快速共享代码 | 高(修改触发全量重编) | 低 | 不推荐新项目使用 |
| 复合构建 | 中大型项目,需要独立构建逻辑 | 低(独立构建) | 中 | 强烈推荐 |
| 约定插件 | 统一模块配置,消除重复 | 取决于载体(推荐复合构建) | 中高 | 项目超过 5 个模块时必用 |
14.7 实战建议:从零搭建现代化构建系统
如果你现在要启动一个新项目,我的建议是这样的:
- 第一步:创建
gradle/libs.versions.toml,把所有依赖版本写进去 - 第二步:创建
build-logic复合构建目录,在里面写约定插件 - 第三步:在约定插件里引用版本目录(通过
libs访问器),把 compileSdk、minSdk、Compose 等通用配置封装好 - 第四步:每个模块的 build.gradle.kts 只保留最少的内容——apply 约定插件,然后写模块特有的依赖
这样做完之后,你的构建脚本会变得极其清爽。升级 AGP?改 TOML 文件里一个版本号就行。新增模块?复制粘贴一行 apply 插件,所有配置自动到位。