22、构建脚本重构:提取公共逻辑、使用闭包与委托、构建脚本模块化、版本统一管理
说实话,很多团队的项目跑了一两年后,build.gradle 文件就开始变得又臭又长。我见过最夸张的一个项目,光 app 模块的构建脚本就写了 800 多行,里面充斥着重复的依赖声明、硬编码的版本号、还有各种复制粘贴的配置块。
嗯,这其实是个坏兆头。构建脚本一旦失控,每次升级依赖或者调整构建逻辑,都像在雷区里跳舞。今天我们就来聊聊怎么给构建脚本做一次彻底的「重构手术」。
为什么构建脚本需要重构?
你想想看,一个多模块的 Android 项目,少说也有三五个模块。如果每个模块都各自维护一份依赖声明,版本号还写死在里面——那升级一次 Support Library 或者迁移到 AndroidX,简直就是噩梦。
我个人习惯把构建脚本看作代码的一部分。它同样需要遵循 DRY(Don't Repeat Yourself)原则,同样需要模块化、可维护、可测试。说白了,构建脚本也是要「设计」的。
提取公共逻辑:别让重复代码害了你
先看一个最常见的反面教材。每个模块的 build.gradle 里都写着类似的配置:
// app/build.gradle
android {
compileSdk 34
defaultConfig {
minSdk 21
targetSdk 34
}
}
// library/build.gradle
android {
compileSdk 34
defaultConfig {
minSdk 21
targetSdk 34
}
}
这有什么问题?问题大了。哪天你想把 minSdk 从 21 升到 23,你得挨个模块改一遍。漏掉一个,编译可能就炸了。
我的做法是:在项目根目录的 build.gradle(或者一个单独的 ext 块)里定义公共配置:
// 根目录 build.gradle
ext {
androidConfig = [
compileSdk: 34,
minSdk : 21,
targetSdk : 34
]
}
然后在每个模块里引用:
// app/build.gradle
android {
compileSdk rootProject.ext.androidConfig.compileSdk
defaultConfig {
minSdk rootProject.ext.androidConfig.minSdk
targetSdk rootProject.ext.androidConfig.targetSdk
}
}
这样改一处,全局生效。我在项目中遇到过好几次因为漏改某个模块的 compileSdk 导致编译失败的情况,自从用了这个方式,再也没出过这种低级错误。
使用闭包与委托:让 Gradle 脚本更「优雅」
Gradle 基于 Groovy(或 Kotlin DSL),闭包和委托是它的核心特性。很多人写脚本时只把它当配置文件用,其实浪费了它的表达能力。
举个例子,我们经常需要为多个模块配置相同的 buildTypes:
// 定义一个闭包,返回一个配置块
def commonBuildConfig = {
buildTypes {
release {
minifyEnabled true
proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
}
debug {
minifyEnabled false
}
}
}
// 在模块中委托执行
android {
compileSdk 34
// 使用 delegate 执行闭包
with(commonBuildConfig) {
delegate = it
call()
}
}
闭包的好处是:你可以把一段配置逻辑「打包」成一个变量,然后在需要的地方「展开」。这比复制粘贴要干净得多。
构建脚本模块化:把大文件拆成小文件
一个 build.gradle 文件如果超过 200 行,就该考虑拆分了。我习惯把构建脚本按职责拆成几个文件:
- dependencies.gradle:统一管理所有依赖声明
- config.gradle:存放 SDK 版本、编译选项等公共配置
- publish.gradle:发布相关的逻辑(如果有)
- quality.gradle:代码检查、测试覆盖率等
然后在主脚本里通过 apply from 引入:
// app/build.gradle
apply from: '../config.gradle'
apply from: '../dependencies.gradle'
android {
// 使用 config.gradle 中定义的变量
compileSdk rootProject.ext.compileSdk
// ...
}
dependencies {
// 使用 dependencies.gradle 中定义的依赖
implementation rootProject.ext.deps.appcompat
implementation rootProject.ext.deps.material
}
这样做的好处很明显:每个文件只关注一件事,修改时不会误伤其他逻辑。我曾经接手过一个项目,构建脚本里混着依赖声明、签名配置、多渠道打包、还有自定义任务——找一行配置要翻半天。拆完之后,清爽多了。
版本统一管理:告别「魔法数字」
版本号散落在各个模块里,是构建脚本最常见的坏味道。我推荐两种主流方案:
方案一:ext 块 + 映射表
// 根目录 build.gradle
ext {
versions = [
kotlin : '1.9.0',
compose : '1.5.0',
retrofit : '2.9.0',
okhttp : '4.11.0'
]
deps = [
kotlinStdlib: "org.jetbrains.kotlin:kotlin-stdlib:${versions.kotlin}",
composeUi : "androidx.compose.ui:ui:${versions.compose}",
retrofit : "com.squareup.retrofit2:retrofit:${versions.retrofit}",
okhttp : "com.squareup.okhttp3:okhttp:${versions.okhttp}"
]
}
方案二:version catalog(推荐)
从 Gradle 7.0 开始,官方推荐使用 Version Catalog。在 gradle/libs.versions.toml 中定义:
[versions]
kotlin = "1.9.0"
compose = "1.5.0"
retrofit = "2.9.0"
[libraries]
kotlin-stdlib = { module = "org.jetbrains.kotlin:kotlin-stdlib", version.ref = "kotlin" }
compose-ui = { module = "androidx.compose.ui:ui", version.ref = "compose" }
retrofit = { module = "com.squareup.retrofit2:retrofit", version.ref = "retrofit" }
[plugins]
kotlin-android = { id = "org.jetbrains.kotlin.android", version.ref = "kotlin" }
然后在模块中引用:
dependencies {
implementation libs.kotlin.stdlib
implementation libs.compose.ui
implementation libs.retrofit
}
libs.versions.toml,放在 gradle 目录下。Gradle 会自动识别并生成 libs 扩展。
我个人更倾向 Version Catalog。它把版本和依赖声明完全分离,IDE 还能提供自动补全。升级依赖时,只需要改一个地方,所有模块同步更新。
知识体系一览
下面这张图概括了构建脚本重构的核心脉络:
避坑指南
重构构建脚本时,有几个坑我踩过,分享给你:
- 不要一次性改太多。 我曾经一口气把整个项目的构建脚本全部重写,结果编译直接挂了三天。建议分模块、分步骤重构,每改一个模块就编译验证一次。
- Version Catalog 的版本号不要写死。 用
version.ref引用,而不是直接写字符串。否则 Catalog 就失去了意义。 - 闭包中的 delegate 要小心。 如果闭包内部调用了
android {}这样的块,确保 delegate 指向了正确的对象。否则会出现「方法找不到」的诡异错误。 - 模块化后注意引入顺序。
apply from的顺序会影响变量是否可用。建议把config.gradle放在最前面。
./gradlew assembleDebug,确保所有模块都能正常编译。然后再提交代码。别问我为什么这么谨慎——问就是吃过亏。
构建脚本重构这件事,说白了就是「把脏活累活干在前面」。前期多花点心思把结构理清楚,后期维护起来会轻松很多。你想想看,每次升级依赖只需要改一个文件,每个模块的配置都一目了然——这种体验,值得你花一个下午去重构。