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

我的习惯:我会把 TOML 文件里的版本号统一放在 [versions] 区,然后用 version.ref 引用。这样升级版本时,只需要改一处。我曾经在一个 40+ 模块的项目里,用这种方式把 AGP 从 7.4 升到 8.2,只花了 10 分钟。

14.2.2 版本目录的优势

  • 类型安全:写错依赖名,IDE 直接报红,不用等到编译
  • 集中管理:所有版本号在一个文件里,一目了然
  • 支持多项目共享:可以发布版本目录到 Maven 仓库,供多个项目使用
  • 不触发重编译:修改 TOML 文件不会导致整个项目重新编译,比 buildSrc 快得多
注意:版本目录文件是纯 TOML 格式,不支持 Kotlin 表达式。如果你需要条件逻辑(比如 debug 和 release 用不同依赖),还是得回到 build.gradle.kts 里处理。

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 项目作为另一个项目的依赖。说白了,就是「项目里套项目」。

具体做法:

  1. 在项目根目录创建一个 build-logic 目录(名字随意)
  2. 在里面放一个完整的 Gradle 项目,包含 build.gradle.kts 和 settings.gradle.kts
  3. 在主项目的 settings.gradle.kts 里引入:includeBuild("build-logic")

这样做的好处是:修改 build-logic 里的代码,不会触发主项目重编译。因为它们是两个独立的构建。只有当你更新了 build-logic 的输出(比如发布新版本的插件),主项目才会重新加载。

我的建议:新项目直接用复合构建。buildSrc 虽然简单,但性能代价太大。如果你已经在用 buildSrc 且项目不大,可以暂时不动;但如果项目超过 10 个模块,我强烈建议迁移到复合构建。

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 依赖——全部自动生效。干净利落。

避坑指南:我曾经在约定插件里写死了 versionCode,结果每次发布都要改插件代码。后来我改成从 gradle.properties 读取版本号,这样运营同学改个配置文件就能发版,不用动代码。

14.5 知识体系总览

下面这张图帮你理清这几个概念的关系:

构建脚本进阶知识体系 构建脚本进阶 版本目录 libs.versions.toml buildSrc Kotlin 常量/函数 复合构建 includeBuild() 约定插件 Plugin<Project> 依赖版本集中管理 类型安全访问器 支持发布到 Maven 共享 Kotlin 代码 自定义任务 工具函数 独立构建,不触发重编译 可发布为独立插件 适合大型项目 封装重复配置 一键 apply 统一构建逻辑 最佳实践组合 版本目录 + 复合构建 + 约定插件 = 可维护、高性能的构建系统

14.6 如何选择?一张表说清楚

方案 适用场景 性能影响 学习成本 我的推荐
版本目录 依赖版本管理 无(纯配置文件) 所有项目必用
buildSrc 小型项目,快速共享代码 高(修改触发全量重编) 不推荐新项目使用
复合构建 中大型项目,需要独立构建逻辑 低(独立构建) 强烈推荐
约定插件 统一模块配置,消除重复 取决于载体(推荐复合构建) 中高 项目超过 5 个模块时必用

14.7 实战建议:从零搭建现代化构建系统

如果你现在要启动一个新项目,我的建议是这样的:

  1. 第一步:创建 gradle/libs.versions.toml,把所有依赖版本写进去
  2. 第二步:创建 build-logic 复合构建目录,在里面写约定插件
  3. 第三步:在约定插件里引用版本目录(通过 libs 访问器),把 compileSdk、minSdk、Compose 等通用配置封装好
  4. 第四步:每个模块的 build.gradle.kts 只保留最少的内容——apply 约定插件,然后写模块特有的依赖

这样做完之后,你的构建脚本会变得极其清爽。升级 AGP?改 TOML 文件里一个版本号就行。新增模块?复制粘贴一行 apply 插件,所有配置自动到位。

最后说一句:构建脚本也是代码,需要重构,需要设计模式。别把它当成「写一次就再也不管」的配置文件。花时间把构建系统搭好,后面省下的时间绝对值得。
公众号:蓝海资料掘金营,微信deep3321