第3章:构建脚本语法变化:Groovy vs Kotlin DSL迁移

从 Groovy 转向 Kotlin DSL,坦白讲是 Gradle 这些年最「折腾」人的变化之一。我刚开始升级项目时,也犯过不少低级错误,比如漏了个括号、类型没写对,导致构建直接崩掉。后来整理出一套迁移 checklist,分享给你。

Groovy → Kotlin DSL 迁移路线图 Groovy 脚本 (build.gradle) 语法差异 引号 / 括号 / lambda 类型安全 Task / Extension 显式 闭包→Lambda it 隐式参数问题 DSL 命名变化 plugins / sourceSets 常见崩溃点 编译报错 / 运行时崩溃 Kotlin DSL (build.gradle.kts) 提示 一次改一部分,别全量迁移

3.1 为什么官方要推 Kotlin DSL?

说白了就两个原因:类型安全和 IDE 补全。Groovy 是动态语言,你在 build.gradle 里写个错别字,只有运行到那一行才知道。Kotlin DSL 在编辑时就帮你检查,我个人的感受——迁移后,配置类错误的调试时间至少省了一半。

核心变化一览表:

对比项 Groovy Kotlin DSL
文件后缀 build.gradle build.gradle.kts
字符串 单引号、双引号均可 双引号必需
闭包 隐式闭包 ( { } ) Lambda 表达式 ( { -> } )
属性赋值 ext.myVar = "value" val myVar by extra("value")
插件声明 apply plugin: 'com.android.application' plugins { id("com.android.application") }

3.2 迁移时最容易踩的坑

3.2.1 括号和引号——最阴间的报错

我记得第一次把 build.gradle 改名为 .kts 后,IDE 直接一片红。原因是 Groovy 里很多地方省略了括号,比如:

// Groovy — 能跑
implementation 'com.android.support:appcompat-v7:28.0.0'

// Kotlin DSL — 必须写括号
implementation("com.android.support:appcompat-v7:28.0.0")

为什么不能省?因为 Kotlin 的函数调用必须加括号。还有 lambda 的写法:

// Groovy
android {
    compileSdkVersion 30
}

// Kotlin DSL — 要注意 it 的使用
android {
    compileSdkVersion = 30   // 注意: compileSdkVersion 是 var,需要 = 赋值
}

我自己的经验: 把所有 dependencies 块里的字符串改成函数调用形式,花了我将近一小时。建议用正则批量替换,效率高很多。

3.2.2 闭包变成 Lambda,it 隐式参数不见了

Groovy 的闭包自带一个隐式参数 it,但在 Kotlin DSL 里,必须显式声明。

// Groovy — 直接能用 it
tasks.whenTaskAdded { task ->
    if (task.name == "assembleDebug") {
        // do something
    }
}

// Kotlin DSL — it 需要声明
tasks.whenTaskAdded {
    if (this.name == "assembleDebug") { ... }  // 注意: this 就是 task
}

⚠️ 避坑指南: 我曾经在迁移中漏掉写 lambda 参数,导致编译通过但运行时任务行为异常。检查 it 被替换成 this 或显式变量名。

3.2.3 extension 属性全部变成 getter/setter

Groovy 里可以直接对 extension 赋值,Kotlin DSL 里你必须通过 setter 或者 val 声明。比如 sourceSets 的写法:

// Groovy
sourceSets {
    main {
        java.srcDirs = ['src/main/java']
    }
}

// Kotlin DSL
sourceSets {
    getByName("main") {
        java.srcDirs("src/main/java")  // 注意不是 =,而是函数调用
    }
}

为什么会这样?因为 Kotlin DSL 的 SourceSetContainer 需要调用 getByName 获取指定名称的配置块。我个人建议统一使用 getByName,更清晰,也便于 IDE 跳转。

3.3 迁移三步骤(实战流程)

  1. 第一步:改文件名 — 把 build.gradle 重命名为 build.gradle.kts,然后同步。此时 IDE 会报错,别慌。
  2. 第二步:逐块修正语法 — 按照报错从少到多修,先修 plugins 块(最简单),再修 android 块,最后修 dependencies 和 task。
  3. 第三步:验证构建 — 执行 ./gradlew assembleDebug 看能否通过。如果报错,优先看 .kts 文件里的红色波浪线。

💡 小技巧: 我通常在迁移时保留一份原 Groovy 文件在另一个目录,方便对照。另外,把报错信息直接复制到 IDE 的“Problems”面板里,双击能跳转到具体行,效率翻倍。

3.4 Kotlin DSL 下常见的崩溃复现场景

错误现象 根因 修复示例
“Unresolved reference: implementation” dependencies 块没用括号 implementation("com.example")
“Type mismatch: inferred type is Int but Unit was expected” 赋值时用了 = 但实际是函数调用 检查是否应该用 compileSdkVersion(30) 而不是 = 30
“Cannot infer type for T” lambda 参数未显式声明 给 lambda 加上显式参数类型
“This declaration is deprecated” 用了旧版 API(如 ext 属性) 迁移到 by extra 方式

你看,迁移本质上就是语法的机械转换,但坑在细节。我团队曾经在合并代码时,有人只改了主模块的 .gradle,忘了改 library 模块,结果 CI 跑了一小时才崩——教训就是统一改完再提交,别拖。Kotlin DSL 虽然初期有点绕,但适应之后,写脚本就跟写业务代码一样舒服了。