14. Gradle Kotlin DSL 进阶:自定义扩展函数、使用Kotlin标准库、委托属性在Gradle中的应用、多项目Kotlin DSL配置
好,咱们继续往前走。上一章我们把 Gradle Kotlin DSL 的基础语法和构建脚本结构捋了一遍。说实话,那只是开胃菜。真正让 Kotlin DSL 变得“香”的,是它背后 Kotlin 语言本身的强大能力。
今天这一章,我会带你深入进去。咱们聊聊怎么用 Kotlin 的特性——扩展函数、标准库、委托属性——来把 Gradle 脚本写得既简洁又优雅。最后再聊聊多项目配置,这可是大型项目的标配。
14.1 自定义扩展函数:让脚本“说人话”
先问个问题:你写 Gradle 脚本时,有没有觉得某些配置特别啰嗦?比如反复写 android { ... } 里面的 compileSdk、defaultConfig?
我个人习惯,遇到重复代码,第一反应就是“能不能封装一下?”在 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 {} 块里直接调用。
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()
我曾经踩过一个坑:忘记关闭文件流,导致构建过程中文件被锁定。后来全部改用 use 或 readText,再也没出过问题。
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。
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
}
}
}
这样做的好处是:所有子模块的 compileSdk 和 minSdk 都保持一致。我曾经见过一个项目,每个模块的 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 本章知识体系
为了让你更直观地理解本章的内容结构,我画了一张图:
14.6 避坑指南
最后,分享几个我踩过的坑:
- 扩展函数的作用域:在
build.gradle.kts里定义的扩展函数,只对当前文件可见。如果想全局共享,放到buildSrc里。 - lazy 委托的线程安全:Gradle 配置阶段是单线程的,所以
lazy默认的线程安全模式(LazyThreadSafetyMode.SYNCHRONIZED)有点浪费。可以改成LazyThreadSafetyMode.NONE提升性能。 - 不要滥用 apply:
apply虽然方便,但嵌套太多层会让代码难以阅读。我一般控制在两层以内。 - buildSrc 的重新编译:修改
buildSrc里的代码后,Gradle 会自动重新编译。但如果你改了版本号,记得同步一下(Sync Now)。
好了,这一章的内容就到这里。你把这些技巧用起来,Gradle 脚本写起来会顺手很多。