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()
    }
}

闭包的好处是:你可以把一段配置逻辑「打包」成一个变量,然后在需要的地方「展开」。这比复制粘贴要干净得多。

小技巧: 在 Kotlin DSL 中,可以用扩展函数 + lambda 实现类似效果。我个人更推荐 Kotlin DSL,类型安全,写起来舒服很多。

构建脚本模块化:把大文件拆成小文件

一个 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
}
注意: Version Catalog 的文件名必须是 libs.versions.toml,放在 gradle 目录下。Gradle 会自动识别并生成 libs 扩展。

我个人更倾向 Version Catalog。它把版本和依赖声明完全分离,IDE 还能提供自动补全。升级依赖时,只需要改一个地方,所有模块同步更新。

知识体系一览

下面这张图概括了构建脚本重构的核心脉络:

构建脚本重构 提取公共逻辑 闭包与委托 脚本模块化 版本统一管理 compileSdk / minSdk 统一 签名配置复用 buildTypes 闭包封装 自定义任务委托 dependencies.gradle config.gradle publish.gradle ext 块映射表 Version Catalog libs.versions.toml 目标:可维护 · 可复用 · 可升级

避坑指南

重构构建脚本时,有几个坑我踩过,分享给你:

  • 不要一次性改太多。 我曾经一口气把整个项目的构建脚本全部重写,结果编译直接挂了三天。建议分模块、分步骤重构,每改一个模块就编译验证一次。
  • Version Catalog 的版本号不要写死。version.ref 引用,而不是直接写字符串。否则 Catalog 就失去了意义。
  • 闭包中的 delegate 要小心。 如果闭包内部调用了 android {} 这样的块,确保 delegate 指向了正确的对象。否则会出现「方法找不到」的诡异错误。
  • 模块化后注意引入顺序。 apply from 的顺序会影响变量是否可用。建议把 config.gradle 放在最前面。
我的习惯: 每次重构完构建脚本,都会在本地跑一次 ./gradlew assembleDebug,确保所有模块都能正常编译。然后再提交代码。别问我为什么这么谨慎——问就是吃过亏。

构建脚本重构这件事,说白了就是「把脏活累活干在前面」。前期多花点心思把结构理清楚,后期维护起来会轻松很多。你想想看,每次升级依赖只需要改一个文件,每个模块的配置都一目了然——这种体验,值得你花一个下午去重构。


公众号:蓝海资料掘金营,微信deep3321