14. Gradle Kotlin DSL 进阶:自定义扩展函数、使用Kotlin标准库、委托属性在Gradle中的应用、多项目Kotlin DSL配置

好,咱们继续往前走。上一章我们把 Gradle Kotlin DSL 的基础语法和构建脚本结构捋了一遍。说实话,那只是开胃菜。真正让 Kotlin DSL 变得“香”的,是它背后 Kotlin 语言本身的强大能力。

今天这一章,我会带你深入进去。咱们聊聊怎么用 Kotlin 的特性——扩展函数、标准库、委托属性——来把 Gradle 脚本写得既简洁又优雅。最后再聊聊多项目配置,这可是大型项目的标配。

14.1 自定义扩展函数:让脚本“说人话”

先问个问题:你写 Gradle 脚本时,有没有觉得某些配置特别啰嗦?比如反复写 android { ... } 里面的 compileSdkdefaultConfig

我个人习惯,遇到重复代码,第一反应就是“能不能封装一下?”在 Kotlin DSL 里,扩展函数就是干这个的。

14.1.1 什么是扩展函数?

说白了,扩展函数就是给一个已有的类“加”一个新方法,但你不用去改这个类的源码。比如给 Project 类加一个 printInfo 方法:

// 在 build.gradle.kts 文件顶部
fun Project.printInfo() {
    println("项目名称: ${this.name}")
    println("项目路径: ${this.path}")
}

// 然后在脚本里直接调用
printInfo()

嗯,这里要注意:Project 是 Gradle 里的核心类。你给它加扩展函数,就等于给所有 Gradle 脚本加了一个全局函数。我在项目中经常用这招来统一打印构建信息。

14.1.2 实战:封装依赖声明

我记得有一次,团队里好几个模块的依赖版本不一致,搞得人头疼。后来我写了个扩展函数,统一管理依赖:

// 在 build.gradle.kts 里
fun DependencyHandlerScope.implementations(vararg dependencies: Any) {
    dependencies.forEach { dep ->
        add("implementation", dep)
    }
}

// 使用
dependencies {
    implementations(
        "androidx.core:core-ktx:1.9.0",
        "androidx.appcompat:appcompat:1.6.1"
    )
}

你看,这样是不是清爽多了?DependencyHandlerScope 是 Gradle 专门为依赖声明提供的上下文。你给它加扩展函数,就能在 dependencies {} 块里直接调用。

小提示:扩展函数不要滥用。如果只是偶尔用一次,没必要封装。我一般是在同一个配置出现 3 次以上时,才会考虑提取。

14.2 使用 Kotlin 标准库:让脚本更“Kotlin”

Kotlin 标准库提供了很多好用的工具函数。在 Gradle 脚本里用上它们,你会发现代码量能减少一半。

14.2.1 集合操作:let、apply、run

我最常用的是 apply。它能让配置代码块更自然:

// 传统写法
val config = BaseConfig()
config.minSdk = 21
config.targetSdk = 33
config.versionCode = 1

// 用 apply
val config = BaseConfig().apply {
    minSdk = 21
    targetSdk = 33
    versionCode = 1
}

为什么会这样?因为 apply 返回的是接收者对象本身,你可以在它上面连续配置。说白了,就是省掉了重复写 config. 的麻烦。

14.2.2 文件操作:use 和 readText

Gradle 脚本里经常要读写文件。Kotlin 标准库的 use 函数能自动关闭资源:

// 读取文件内容
val content = File("version.txt").useLines { lines ->
    lines.joinToString("\n")
}

// 或者更简单
val version = File("version.txt").readText().trim()

我曾经踩过一个坑:忘记关闭文件流,导致构建过程中文件被锁定。后来全部改用 usereadText,再也没出过问题。

14.2.3 字符串模板:告别字符串拼接

val appName = "MyApp"
val versionName = "1.0.0"

// 传统 Java 风格
println("App: " + appName + ", Version: " + versionName)

// Kotlin 风格
println("App: $appName, Version: $versionName")

你想想看,哪个更直观?字符串模板不仅简洁,还避免了拼接时漏掉加号这种低级错误。

14.3 委托属性在 Gradle 中的应用

委托属性是 Kotlin 的一个高级特性。在 Gradle 脚本里,它主要用来做“懒加载”和“属性观察”。

14.3.1 lazy 委托:按需初始化

有些配置只在特定任务中用到,没必要每次都初始化。用 lazy 委托就能做到“用的时候才创建”:

val myConfig by lazy {
    println("初始化配置...")
    loadConfigFromFile("config.json")
}

// 第一次访问时才会执行 lazy 块
println(myConfig)

我在写自定义插件时,经常用 lazy 来加载一些重量级的资源,比如签名文件、密钥等。这样能加快 Gradle 的配置阶段速度。

14.3.2 observable 委托:监听属性变化

如果你想在某个属性被修改时做一些额外操作,可以用 observable

var versionCode: Int by Delegates.observable(1) { prop, old, new ->
    println("版本号从 $old 变更为 $new")
    // 可以在这里做校验,比如版本号不能减小
    if (new < old) {
        throw IllegalArgumentException("版本号不能减小!")
    }
}

嗯,这里要注意:observable 的回调是在属性被赋值之后触发的。如果你想在赋值之前拦截,可以用 vetoable

警告:委托属性虽然好用,但不要过度使用。特别是在 Gradle 配置阶段,过多的 lazy 委托可能会导致难以追踪的初始化顺序问题。我建议只在确实需要延迟初始化或属性监听时使用。

14.4 多项目 Kotlin DSL 配置

大型 Android 项目通常都是多模块的。用 Kotlin DSL 来管理多项目配置,能让你享受到类型安全和代码提示的好处。

14.4.1 根项目的 build.gradle.kts

根项目的脚本主要负责两件事:声明子项目和配置公共插件。

// 根目录的 build.gradle.kts
plugins {
    id("com.android.application") version "8.1.0" apply false
    id("com.android.library") version "8.1.0" apply false
    id("org.jetbrains.kotlin.android") version "1.9.0" apply false
}

// 声明子项目
include(":app")
include(":library:core")
include(":library:network")

注意 apply false 这个写法。它的意思是“声明这个插件,但不要应用到当前项目”。子项目需要时再自己应用。

14.4.2 统一配置:subprojects 和 allprojects

我习惯在根项目里统一配置所有子项目的公共属性:

// 根目录的 build.gradle.kts
subprojects {
    apply(plugin = "org.jetbrains.kotlin.android")

    android {
        compileSdk = 33

        defaultConfig {
            minSdk = 21
            targetSdk = 33
        }
    }
}

这样做的好处是:所有子模块的 compileSdkminSdk 都保持一致。我曾经见过一个项目,每个模块的 compileSdk 都不一样,结果编译时各种兼容性问题。统一配置后,这类问题就再也没出现过。

14.4.3 使用 buildSrc 共享代码

当你的扩展函数和常量越来越多时,放在 build.gradle.kts 文件里会显得很乱。这时候,buildSrc 就派上用场了。

在项目根目录创建 buildSrc/src/main/kotlin 目录,然后放一个 Dependencies.kt 文件:

// buildSrc/src/main/kotlin/Dependencies.kt
object Versions {
    const val compileSdk = 33
    const val minSdk = 21
    const val targetSdk = 33
}

object Libs {
    const val coreKtx = "androidx.core:core-ktx:1.9.0"
    const val appcompat = "androidx.appcompat:appcompat:1.6.1"
}

然后在子项目的 build.gradle.kts 里直接引用:

android {
    compileSdk = Versions.compileSdk
}

dependencies {
    implementation(Libs.coreKtx)
    implementation(Libs.appcompat)
}

你想想看,这样是不是既清晰又容易维护?所有版本号都集中在一个地方,改起来也方便。

14.5 本章知识体系

为了让你更直观地理解本章的内容结构,我画了一张图:

Gradle Kotlin DSL 进阶知识体系 自定义扩展函数 给 Project 类加方法 封装依赖声明 统一配置逻辑 减少重复代码 Kotlin 标准库 集合操作 (apply/let) 文件操作 (use/readText) 字符串模板 简化脚本编写 委托属性 lazy 懒加载 observable 监听 vetoable 拦截 按需初始化 多项目配置 根项目配置 subprojects 统一 buildSrc 共享 模块化管理 核心目标 让 Gradle 脚本更简洁、更安全、更易于维护 四个模块相互独立,但可以组合使用,达到最佳效果 💡 个人经验:buildSrc + 扩展函数 是最佳实践组合

14.6 避坑指南

最后,分享几个我踩过的坑:

  • 扩展函数的作用域:在 build.gradle.kts 里定义的扩展函数,只对当前文件可见。如果想全局共享,放到 buildSrc 里。
  • lazy 委托的线程安全:Gradle 配置阶段是单线程的,所以 lazy 默认的线程安全模式(LazyThreadSafetyMode.SYNCHRONIZED)有点浪费。可以改成 LazyThreadSafetyMode.NONE 提升性能。
  • 不要滥用 applyapply 虽然方便,但嵌套太多层会让代码难以阅读。我一般控制在两层以内。
  • buildSrc 的重新编译:修改 buildSrc 里的代码后,Gradle 会自动重新编译。但如果你改了版本号,记得同步一下(Sync Now)。

好了,这一章的内容就到这里。你把这些技巧用起来,Gradle 脚本写起来会顺手很多。


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