Gradle 的 Kotlin DSL 提供了一种替代传统的 Groovy DSL 的方式,在支持的 IDE 中提供增强的编辑体验,包括更好的内容辅助、重构和文档等功能。

本章探讨 Kotlin DSL 的关键结构,并演示如何使用它们与 Gradle API 交互。

如果您有兴趣将现有 Gradle 构建迁移到 Kotlin DSL,请查看专门的迁移页面

先决条件

  • 嵌入式 Kotlin 编译器可在 Linux、macOS、Windows、Cygwin、FreeBSD 和 Solaris 的 x86-64 架构上工作。

  • 建议熟悉 Kotlin 语法和基本语言特性。请参阅Kotlin 文档Kotlin Koans 以学习基础知识。

  • 强烈建议使用 plugins {} 块声明 Gradle 插件,因为它可以显著改善编辑体验。

IDE 支持

Kotlin DSL 完全受 IntelliJ IDEA 和 Android Studio 支持。虽然其他 IDE 缺乏编辑 Kotlin DSL 文件的高级工具,但您仍然可以导入基于 Kotlin DSL 的构建并像往常一样使用它们。

构建导入 语法高亮 1 语义编辑器 2

IntelliJ IDEA

Android Studio

Eclipse IDE

CLion

Apache NetBeans

Visual Studio Code (LSP)

Visual Studio

1 Gradle Kotlin DSL 脚本中的 Kotlin 语法高亮
2 Gradle Kotlin DSL 脚本中的代码 完成、 跳转到源码、 文档、 重构等…

如限制中所述,您必须使用 Gradle 模型导入您的项目,才能在 IntelliJ IDEA 中为 Kotlin DSL 脚本启用内容辅助和重构工具。

配置时间慢的构建可能会影响 IDE 响应速度,因此请查看性能部分以帮助解决此类问题。

自动构建导入 vs. 自动重载脚本依赖

IntelliJ IDEA 和 Android Studio 都会检测到您何时对构建逻辑进行更改并提供两个建议

  1. 再次导入整个构建

    IntelliJ IDEA
    IntelliJ IDEA
  2. 编辑构建脚本时重载脚本依赖

    Reload script dependencies

我们建议禁用自动构建导入,同时启用自动重载脚本依赖。这种方法在编辑 Gradle 脚本时提供早期反馈,同时让您控制何时将整个构建设置与 IDE 同步。

请参阅故障排除部分了解更多信息。

Kotlin DSL 脚本

就像基于 Groovy 的对应物一样,Kotlin DSL 构建在 Gradle 的 Java API 之上。Kotlin DSL 脚本中的一切都是 Kotlin 代码,由 Gradle 编译和执行。构建脚本中的许多对象、函数和属性来自 Gradle API 和应用插件的 API。

使用Kotlin DSL 参考搜索来探索可用成员。

脚本文件名

  • Groovy DSL 脚本文件使用 .gradle 文件名扩展名。

  • Kotlin DSL 脚本文件使用 .gradle.kts 文件名扩展名。

要激活 Kotlin DSL,请为您的构建脚本使用 .gradle.kts 扩展名,而不是 .gradle。这也适用于设置文件(例如,settings.gradle.kts)和初始化脚本

您可以在同一个构建中混合使用 Groovy DSL 和 Kotlin DSL 脚本。例如,一个 Kotlin DSL 构建脚本可以应用一个 Groovy DSL 脚本,并且多项目构建中的不同项目可以使用其中任何一种。

为了改进 IDE 支持,我们建议遵循以下约定

  • 使用模式 *.settings.gradle.kts 命名 settings 脚本(或任何由 Gradle Settings 对象支持的脚本)。这包括从 settings 脚本应用的脚本插件。

  • 使用模式 *.init.gradle.kts 或直接使用 init.gradle.kts 命名初始化脚本

这有助于 IDE 识别“支持”脚本的对象,无论是 ProjectSettings 还是 Gradle

隐式导入

所有 Kotlin DSL 构建脚本都附带隐式导入,包括

  • 默认 Gradle API 导入

  • Kotlin DSL API,其中包括来自以下包的类型

    • org.gradle.kotlin.dsl

    • org.gradle.kotlin.dsl.plugins.dsl

    • org.gradle.kotlin.dsl.precompile

避免使用内部 Kotlin DSL API

在插件和构建脚本中使用内部 Kotlin DSL API 可能会在 Gradle 或插件更新时破坏构建。

Kotlin DSL API 扩展了公共 Gradle API,增加了在上述包(但不包括其子包)中找到的相应 API 文档中列出的类型。

编译警告

Gradle Kotlin DSL 脚本在构建的配置阶段由 Gradle 编译。

Kotlin 编译器发现的废弃警告在编译脚本时会在控制台上报告

> Configure project :
w: build.gradle.kts:4:5: 'getter for uploadTaskName: String!' is deprecated. Deprecated in Java

通过设置 Gradle 属性 org.gradle.kotlin.dsl.allWarningsAsErrorstrue,可以将您的构建配置为在脚本编译期间发出的任何警告时失败。

gradle.properties
org.gradle.kotlin.dsl.allWarningsAsErrors=true

类型安全的模型访问器

Groovy DSL 允许您按名称引用许多构建模型元素,即使它们是在运行时定义的,例如命名配置或源代码集。

例如,应用 Java 插件时,您可以通过 configurations.implementation 访问 implementation 配置。

Kotlin DSL 用类型安全的模型访问器取代了这种动态解析,这些访问器适用于插件贡献的模型元素。

理解何时可以使用类型安全的模型访问器

Kotlin DSL 当前提供各种类型的类型安全的模型访问器集,每种都针对不同的范围量身定制。

对于主项目构建脚本和预编译项目脚本插件

类型安全的模型访问器 示例

依赖和 Artifact 配置

implementationruntimeOnly(由 Java 插件贡献)

项目扩展和约定,以及其上的扩展

sourceSets

dependenciesrepositories 容器上的扩展,以及其上的扩展

testImplementation(由 Java 插件贡献),mavenCentral

tasksconfigurations 容器中的元素

compileJava(由 Java 插件贡献),test

项目扩展容器中的元素

由 Java 插件贡献并添加到 sourceSets 容器中的源代码集:sourceSets.main.java { setSrcDirs(listOf("src/main/java")) }

对于主项目 settings 脚本和预编译 settings 脚本插件

类型安全的模型访问器 示例

Settings 插件贡献的项目扩展和约定,以及其上的扩展

pluginManagement, dependencyResolutionManagement

初始化脚本和脚本插件没有类型安全的模型访问器。这些限制将在未来的 Gradle 版本中移除。

可用的类型安全的模型访问器集在评估脚本主体之前确定,紧接在 plugins {} 块之后。在此之后贡献的模型元素,例如在构建脚本中定义的配置,将无法使用类型安全的模型访问器。

build.gradle.kts
// Applies the Java plugin
plugins {
    id("java")
}

repositories {
    mavenCentral()
}

// Access to 'implementation' (contributed by the Java plugin) works here:
dependencies {
    implementation("org.apache.commons:commons-lang3:3.12.0")
    testImplementation("org.junit.jupiter:junit-jupiter:5.10.0")
    testRuntimeOnly("org.junit.platform:junit-platform-launcher") // Add this if needed for runtime
}

// Add a custom configuration
configurations.create("customConfiguration")
// Type-safe accessors for 'customConfiguration' will NOT be available because it was created after the plugins block
dependencies {
    customConfiguration("com.google.guava:guava:32.1.2-jre") // ❌ Error: No type-safe accessor for 'customConfiguration'
}

但是,这意味着您可以为由父项目应用的插件贡献的任何模型元素使用类型安全的访问器。

以下项目构建脚本演示了如何使用类型安全的访问器访问各种配置、扩展和其他元素

build.gradle.kts
plugins {
    `java-library`
}

dependencies {                              (1)
    api("junit:junit:4.13")
    implementation("junit:junit:4.13")
    testImplementation("junit:junit:4.13")
}

configurations {                            (1)
    implementation {
        resolutionStrategy.failOnVersionConflict()
    }
}

sourceSets {                                (2)
    main {                                  (3)
        java.srcDir("src/core/java")
    }
}

java {                                      (4)
    sourceCompatibility = JavaVersion.VERSION_11
    targetCompatibility = JavaVersion.VERSION_11
}

tasks {
    test {                                  (5)
        testLogging.showExceptions = true
        useJUnit()
    }
}
1 使用由Java 库插件贡献的 apiimplementationtestImplementation 依赖配置的类型安全访问器
2 使用访问器配置 sourceSets 项目扩展
3 使用访问器配置 main 源代码集
4 使用访问器配置 main 源代码集的 java 源代码
5 使用访问器配置 test 任务

您的 IDE 知道类型安全的访问器,并将它们包含在其建议中。

这既适用于构建脚本的顶层(大多数插件扩展都添加到 Project 对象),也适用于配置扩展的块内部。

请注意,configurationstaskssourceSets 等容器元素的访问器利用了 Gradle 的配置规避 API。例如,在 tasks 上,访问器的类型是 TaskProvider<T>,提供底层任务的延迟引用和延迟配置。

这里有一些示例说明了配置规避何时适用

build.gradle.kts
tasks.test {
    // lazy configuration
    useJUnitPlatform()
}

// Lazy reference
val testProvider: TaskProvider<Test> = tasks.test

testProvider {
    // lazy configuration
}

// Eagerly realized Test task, defeats configuration avoidance if done out of a lazy context
val test: Test = tasks.test.get()

对于所有其他容器,元素的访问器的类型是 NamedDomainObjectProvider<T>,提供相同的行为

build.gradle.kts
val mainSourceSetProvider: NamedDomainObjectProvider<SourceSet> = sourceSets.named("main")

理解当类型安全的模型访问器不可用时该怎么办

考虑上面显示的示例构建脚本,它演示了类型安全访问器的使用。下面的示例是相同的,只是它使用 apply() 方法应用插件。

在这种情况下,构建脚本无法使用类型安全的访问器,因为 apply() 调用发生在构建脚本主体中。您必须改用其他技术,如下所示

build.gradle.kts
apply(plugin = "java-library")

dependencies {
    "api"("junit:junit:4.13")
    "implementation"("junit:junit:4.13")
    "testImplementation"("junit:junit:4.13")
}

configurations {
    "implementation" {
        resolutionStrategy.failOnVersionConflict()
    }
}

configure<SourceSetContainer> {
    named("main") {
        java.srcDir("src/core/java")
    }
}

configure<JavaPluginExtension> {
    sourceCompatibility = JavaVersion.VERSION_11
    targetCompatibility = JavaVersion.VERSION_11
}

tasks {
    named<Test>("test") {
        testLogging.showExceptions = true
    }
}

对于以下项贡献的模型元素,类型安全的访问器不可用

  • 通过 apply(plugin = "id") 方法应用的插件。

  • 项目构建脚本。

  • 脚本插件,通过 apply(from = "script-plugin.gradle.kts")

  • 通过跨项目配置应用的插件。

在用 Kotlin 实现的二进制 Gradle 插件中,您不能使用类型安全的访问器。

如果找不到类型安全的访问器,请回退到使用对应类型的常规 API。为此,您需要知道所配置模型元素的名称和/或类型。现在我们将详细检查脚本,向您展示如何发现这些信息。

Artifact 配置

以下示例演示了如何在没有类型安全访问器的情况下引用和配置 Artifact 配置

build.gradle.kts
apply(plugin = "java-library")

dependencies {
    "api"("junit:junit:4.13")
    "implementation"("junit:junit:4.13")
    "testImplementation"("junit:junit:4.13")
}

configurations {
    "implementation" {
        resolutionStrategy.failOnVersionConflict()
    }
}

代码看起来与类型安全访问器相似,不同之处在于配置名称是字符串字面量。您可以在依赖声明和 configurations {} 块中使用字符串字面量作为配置名称。

虽然 IDE 无法帮助您发现可用的配置,但您可以在相应插件的文档中或通过运行 ./gradlew dependencies 来查找它们。

项目扩展和约定

项目扩展和约定既有名称也有唯一的类型。但是,Kotlin DSL 只需知道类型即可配置它们。

以下示例显示了原始示例构建脚本中的 sourceSets {}java {} 块。使用相应的类型调用configure<T>() 函数

build.gradle.kts
apply(plugin = "java-library")

configure<SourceSetContainer> {
    named("main") {
        java.srcDir("src/core/java")
    }
}

configure<JavaPluginExtension> {
    sourceCompatibility = JavaVersion.VERSION_11
    targetCompatibility = JavaVersion.VERSION_11
}

请注意,sourceSetsProject 上的 Gradle 扩展,类型为 SourceSetContainer,而 javaProject 上的扩展,类型为 JavaPluginExtension

您可以通过查阅应用插件的文档或运行 ./gradlew kotlinDslAccessorsReport 来发现可用的扩展和约定。该报告生成访问应用插件贡献的模型元素所需的 Kotlin 代码,同时提供名称和类型。

作为最后一种手段,您可以检查插件的源代码,尽管在大多数情况下这应该不是必需的。

如果您只需要对扩展或约定的引用而无需配置它,或者如果您想执行单行配置,也可以使用the<T>() 函数。

build.gradle.kts
the<SourceSetContainer>()["main"].java.srcDir("src/main/java")

上面的片段也演示了配置作为容器的项目扩展元素的其中一种方法。

项目扩展容器中的元素

基于容器的项目扩展,例如 SourceSetContainer,允许您配置它们所持有的元素。

在我们的示例构建脚本中,我们想在源代码集容器中配置名为 main 的源代码集。我们可以通过使用 named() 方法而不是访问器来完成此操作。

build.gradle.kts
apply(plugin = "java-library")

configure<SourceSetContainer> {
    named("main") {
        java.srcDir("src/core/java")
    }
}

基于容器的项目扩展中的所有元素都有名称,因此您可以在所有此类情况下使用此技术。

对于项目扩展和约定,您可以通过查阅应用插件的文档或运行 ./gradlew kotlinDslAccessorsReport 来发现任何容器中存在哪些元素。

作为最后一种手段,您也可以查阅插件的源代码以了解其作用。

任务

任务不是通过基于容器的项目扩展来管理的,但它们是行为方式相似的容器的一部分。

这意味着您可以像配置源代码集一样配置任务。以下示例说明了这种方法

build.gradle.kts
apply(plugin = "java-library")

tasks {
    named<Test>("test") {
        testLogging.showExceptions = true
    }
}

我们正在使用 Gradle API 按名称和类型引用任务,而不是使用访问器。

请注意,有必要明确指定任务的类型。如果您不这样做,脚本将无法编译,因为推断的类型将是 Task 而不是 Test,并且 testLogging 属性是 Test 任务类型特有的。

但是,如果您只需要配置所有任务共有的属性或调用方法(即在 Task 接口上声明的那些),则可以省略类型。

您可以通过运行 ./gradlew tasks 来发现有哪些任务可用。

要找出给定任务的类型,请运行 ./gradlew help --task <taskName>,如下所示

❯ ./gradlew help --task test
...
Type
     Test (org.gradle.api.tasks.testing.Test)

IDE 可以帮助您完成所需的导入,因此您只需要类型的简单名称,无需包名部分。在这种情况下,无需导入 Test 任务类型,因为它属于 Gradle API 的一部分,因此是隐式导入的

关于约定 (conventions)

一些 Gradle 核心插件通过所谓的 约定 (convention) 对象暴露可配置性。这些约定对象的作用与 扩展 (extensions) 类似,并且现在已被扩展取代。约定已被废弃,因此在编写新插件时请避免使用约定对象。

如上所述,Kotlin DSL 只为 Project 上的约定对象提供访问器。但是,在某些情况下,您可能需要与使用其他类型上的约定对象的 Gradle 插件交互。为此,Kotlin DSL 提供了 withConvention(T::class) {} 扩展函数

build.gradle.kts
sourceSets {
    main {
        withConvention(CustomSourceSetConvention::class) {
            someOption = "some value"
        }
    }
}

这种技术主要适用于尚未迁移到扩展的语言插件添加的源代码集。

使用容器对象

Gradle 构建模型广泛使用容器对象(或简称为“容器”)。

例如,configurationstasks 是容器,分别持有 ConfigurationTask 对象。社区插件也贡献容器,例如由 Android 插件贡献的 android.buildTypes 容器。

Kotlin DSL 为构建作者提供了多种与容器交互的方式。我们将探索每种方法,以 tasks 容器为例。

在配置支持容器上的现有元素时,您可以利用另一节中描述的类型安全访问器。该节还解释了哪些容器支持类型安全访问器。

使用容器 API

Gradle 中的所有容器都实现了 NamedDomainObjectContainer<DomainObjectType>。有些容器可以持有不同类型的对象,并实现了 PolymorphicDomainObjectContainer<BaseType>。与容器交互的最简单方法是通过这些接口。

以下示例演示了如何使用 named() 方法配置现有任务,以及使用 register() 方法创建新任务

build.gradle.kts
tasks.named("check")                    (1)
tasks.register("myTask1")               (2)

tasks.named<JavaCompile>("compileJava") (3)
tasks.register<Copy>("myCopy1")         (4)

tasks.named("assemble") {               (5)
    dependsOn(":myTask1")
}
tasks.register("myTask2") {             (6)
    description = "Some meaningful words"
}

tasks.named<Test>("test") {             (7)
    testLogging.showStackTraces = true
}
tasks.register<Copy>("myCopy2") {       (8)
    from("source")
    into("destination")
}
1 获取名为 check 的现有任务的 Task 类型引用
2 注册一个名为 myTask1 的新的无类型任务
3 获取名为 compileJava 的现有任务的 JavaCompile 类型引用
4 注册一个名为 myCopy1Copy 类型新任务
5 获取对名为 assemble 的现有(无类型)任务的引用并配置它—— 使用此语法您只能配置 Task 上可用的属性和方法
6 注册一个名为 myTask2 的新的无类型任务并配置它—— 在这种情况下您只能配置 Task 上可用的属性和方法
7 获取对名为 test 的现有 Test 类型任务的引用并配置它—— 在这种情况下,您可以访问指定类型的属性和方法
8 注册一个名为 myCopy2Copy 类型新任务并配置它
上面的示例依赖于配置规避 API。如果您需要或想要立即配置或注册容器元素,只需将 named() 替换为 getByName(),并将 register() 替换为 create()

使用 Kotlin 委托属性

另一种与容器交互的方式是通过 Kotlin 委托属性。如果您需要对容器元素的引用,并且可以在构建的其他地方使用该引用,那么这种方式特别有用。此外,Kotlin 委托属性可以通过 IDE 重构轻松重命名。

以下示例实现了与上一节中相同的效果,但它使用委托属性并重用这些引用,而不是使用字符串字面量任务路径

build.gradle.kts
val check by tasks.existing
val myTask1 by tasks.registering

val compileJava by tasks.existing(JavaCompile::class)
val myCopy1 by tasks.registering(Copy::class)

val assemble by tasks.existing {
    dependsOn(myTask1)  (1)
}
val myTask2 by tasks.registering {
    description = "Some meaningful words"
}

val test by tasks.existing(Test::class) {
    testLogging.showStackTraces = true
}
val myCopy2 by tasks.registering(Copy::class) {
    from("source")
    into("destination")
}
1 使用对 myTask1 任务的引用,而不是任务路径
上面的示例依赖于配置规避 API。如果您需要或想要立即配置或注册容器元素,只需将existing() 替换为getting(),并将registering() 替换为creating()

一起配置多个容器元素

在配置容器的多个元素时,您可以将交互操作分组到一个块中,以避免在每次交互时重复容器的名称。

这个例子演示了类型安全访问器、容器 API 和 Kotlin 委托属性的组合

build.gradle.kts
tasks {
    test {
        testLogging.showStackTraces = true
    }
    val myCheck by registering {
        doLast { /* assert on something meaningful */ }
    }
    check {
        dependsOn(myCheck)
    }
    register("myHelp") {
        doLast { /* do something helpful */ }
    }
}

使用运行时属性

Gradle 在运行时定义了两种主要的属性来源:项目属性额外属性

Kotlin DSL 为使用这些属性类型提供了特定的语法,我们将在以下部分进行探讨。

项目属性

Kotlin DSL 允许你通过 Kotlin 委托属性绑定来访问项目属性。

以下代码片段演示了针对几个项目属性使用此技术的方法,其中一个属性必须被定义

build.gradle.kts
val myProperty: String by project  (1)
val myNullableProperty: String? by project (2)
1 通过 myProperty 委托属性使 myProperty 项目属性可用 — 在这种情况下,项目属性必须存在,否则构建脚本尝试使用 myProperty 值时构建将失败
2 myNullableProperty 项目属性也执行相同操作,但只要你检查 null(标准Kotlin null 安全规则适用),使用 myNullableProperty 值时构建不会失败

同样的方法也适用于 settings 和 initialization 脚本,不同之处在于你分别使用 by settingsby gradle 代替 by project

额外属性

额外属性可用于任何实现 ExtensionAware 接口的对象。

在 Kotlin DSL 中,你可以通过委托属性访问和创建额外属性,使用 by extra 语法,如下例所示

build.gradle.kts
val myNewProperty by extra("initial value")  (1)
val myOtherNewProperty by extra { "calculated initial value" }  (2)

val myExtraProperty: String by extra  (3)
val myExtraNullableProperty: String? by extra  (4)
1 在当前上下文(本例中为项目)中创建一个名为 myNewProperty 的新额外属性,并将其初始化为值 "initial value",这也会确定属性的类型
2 创建一个新的额外属性,其初始值由提供的 lambda 计算得出
3 将当前上下文(本例中为项目)中现有的额外属性绑定到 myProperty 引用
4 执行与上一行相同的操作,但允许属性具有 null 值

此方法适用于所有 Gradle 脚本:项目构建脚本、脚本插件、settings 脚本和 initialization 脚本。

你还可以使用以下语法从子项目访问根项目上的额外属性

my-sub-project/build.gradle.kts
val myNewProperty: String by rootProject.extra  (1)
1 将根项目的 myNewProperty 额外属性绑定到同名引用

额外属性不仅仅限于项目。例如,Task 扩展了 ExtensionAware,因此你也可以向任务附加额外属性。

以下示例演示了如何在 test 任务上定义一个新的 myNewTaskProperty,然后使用该属性初始化另一个任务

build.gradle.kts
tasks {
    test {
        val reportType by extra("dev")  (1)
        doLast {
            // Use 'suffix' for post-processing of reports
        }
    }

    register<Zip>("archiveTestReports") {
        val reportType: String by test.get().extra  (2)
        archiveAppendix = reportType
        from(test.get().reports.html.outputLocation)
    }
}
1 test 任务上创建一个新的 reportType 额外属性
2 使 test 任务的 reportType 额外属性可用于配置 archiveTestReports 任务

如果你乐于使用 eager configuration 而不是 configuration avoidance API,则可以为报告类型使用一个单一的“全局”属性,如下所示

build.gradle.kts
tasks.test {
    doLast { /* ... */ }
}

val testReportType by tasks.test.get().extra("dev")  (1)

tasks.create<Zip>("archiveTestsReports") {
    archiveAppendix = testReportType  (2)
    from(test.reports.html.outputLocation)
}
1 test 任务上创建并初始化一个额外属性,将其绑定到“全局”属性
2 使用“全局”属性初始化 archiveTestReports 任务

额外属性还有最后一种语法,将 extra 视为 map。我们通常不建议使用此方法,因为它绕过了 Kotlin 的类型检查并限制了 IDE 支持。但是,它比委托属性语法更简洁,如果你只需要设置额外属性而无需稍后引用它,则可以使用此方法。

以下是一个简单的示例,演示如何使用 map 语法设置和读取额外属性

build.gradle.kts
extra["myNewProperty"] = "initial value"  (1)

tasks.register("myTask") {
    doLast {
        println("Property: ${project.extra["myNewProperty"]}")  (2)
    }
}
1 创建一个名为 myNewProperty 的新项目额外属性并设置其值
2 从我们创建的项目额外属性中读取值 — 注意 extra[…​] 上的 project. 限定符,否则 Gradle 将假定我们要从任务中读取额外属性

使用 Gradle 类型

PropertyProviderNamedDomainObjectProvider 是表示值和对象的延迟及惰性评估的类型。Kotlin DSL 为使用这些类型提供了专门的语法。

使用 Property

property 表示一个可以惰性设置和读取的值

  • 设置值:property.set(value)property = value

  • 访问值:property.get()

  • 使用委托语法:val propValue: String by property

build.gradle.kts
val myProperty: Property<String> = project.objects.property(String::class.java)

myProperty.set("Hello, Gradle!") // Set the value
println(myProperty.get())        // Access the value

// Using delegate syntax
val propValue: String by myProperty
println(propValue)

// Using lazy syntax
myProperty = "Hi, Gradle!" // Set the value
println(myProperty.get())  // Access the value

使用 Provider

provider 表示一个只读、惰性评估的值

  • 访问值:provider.get()

  • 链式调用:provider.map { transform(it) }

build.gradle.kts
val versionProvider: Provider<String> = project.provider { "1.0.0" }

println(versionProvider.get()) // Access the value

// Chaining transformations
val majorVersion: Provider<String> = versionProvider.map { it.split(".")[0] }
println(majorVersion.get()) // Prints: "1"

使用 NamedDomainObjectProvider

named domain object provider 表示从 Gradle 容器(如 tasks 或 extensions)中惰性评估的命名对象

  • 访问对象:namedObjectProvider.get()

  • 配置对象:namedObjectProvider.configure { …​ }

build.gradle.kts
val myTaskProvider: NamedDomainObjectProvider<Task> = tasks.named("build")

// Configuring the task
myTaskProvider.configure {
    doLast {
        println("Build task completed!")
    }
}

// Accessing the task
val myTask: Task = myTaskProvider.get()

惰性属性赋值

Gradle 的 Kotlin DSL 支持使用 = 运算符进行惰性属性赋值。

使用惰性属性时,惰性属性赋值减少了冗余。它适用于公开被视为 final(没有 setter)且类型为 PropertyConfigurableFileCollection 的属性。由于属性必须是 final,我们通常建议避免为惰性类型的属性使用自定义 setter,如果可能,通过抽象 getter 实现此类属性。

在 Kotlin DSL 中,使用 = 运算符是调用 set() 的首选方式

build.gradle.kts
java {
    toolchain {
        languageVersion = JavaLanguageVersion.of(17)
    }
}

abstract class WriteJavaVersionTask : DefaultTask() {
    @get:Input
    abstract val javaVersion: Property<String>
    @get:OutputFile
    abstract val output: RegularFileProperty

    @TaskAction
    fun execute() {
        output.get().asFile.writeText("Java version: ${javaVersion.get()}")
    }
}

tasks.register<WriteJavaVersionTask>("writeJavaVersion") {
    javaVersion.set("17") (1)
    javaVersion = "17" (2)
    javaVersion = java.toolchain.languageVersion.map { it.toString() } (3)
    output = layout.buildDirectory.file("writeJavaVersion/javaVersion.txt")
}
1 使用 .set() 方法设置值
2 使用 = 运算符进行惰性属性赋值设置值
3 = 运算符也可用于赋值惰性值

IDE 支持

IntelliJ 2022.3 及更高版本以及 Android Studio Giraffe 及更高版本支持惰性属性赋值。

Kotlin DSL 插件

Kotlin DSL 插件提供了一种便捷的方式来开发基于 Kotlin 的项目,这些项目贡献构建逻辑。这包括buildSrc 项目included buildsGradle 插件

该插件通过执行以下操作来实现此目的

  • 应用Kotlin 插件,该插件添加了编译 Kotlin 源文件的支持。

  • kotlin-stdlibkotlin-reflectgradleKotlinDsl() 依赖项添加到 compileOnlytestImplementation 配置中,从而使你能够在 Kotlin 代码中使用这些 Kotlin 库和 Gradle API。

  • 使用与 Kotlin DSL 脚本相同的设置配置 Kotlin 编译器,确保构建逻辑与这些脚本之间的一致性

  • 启用对预编译脚本插件的支持。

每个 Gradle 版本都应与特定版本的 kotlin-dsl 插件一起使用。不保证任意 Gradle 版本与 kotlin-dsl 插件版本之间的兼容性。使用非预期版本的 kotlin-dsl 插件将发出警告,并可能导致难以诊断的问题。

这是使用该插件所需的基本配置

buildSrc/build.gradle.kts
plugins {
    `kotlin-dsl`
}

repositories {
    // The org.jetbrains.kotlin.jvm plugin requires a repository
    // where to download the Kotlin compiler dependencies from.
    mavenCentral()
}

Kotlin DSL 插件利用了Java Toolchains。默认情况下,代码将面向 Java 8。你可以通过定义项目要使用的 Java toolchain 来更改它

buildSrc/src/main/kotlin/myproject.java-conventions.gradle.kts
java {
    toolchain {
        languageVersion = JavaLanguageVersion.of(11)
    }
}

嵌入式 Kotlin

Gradle 嵌入了 Kotlin,以便为基于 Kotlin 的脚本提供支持。

Kotlin 版本

Gradle 附带了 kotlin-compiler-embeddable 以及匹配版本的 kotlin-stdlibkotlin-reflect 库。有关详细信息,请参阅 Gradle 兼容性矩阵的 Kotlin 部分。来自这些模块的 kotlin 包可通过 Gradle classpath 可见。

Kotlin 提供的兼容性保证适用于向后兼容和向前兼容。

向后兼容性

我们的方法是仅在主要的 Gradle 版本发布时进行向后不兼容的 Kotlin 升级。我们清晰地记录了每个版本附带的 Kotlin 版本,并在主要版本发布前宣布升级计划。

旨在与旧版 Gradle 保持兼容性的插件作者必须将其 API 使用限制在这些版本支持的范围内。这与在 Gradle 中使用任何新 API 没有什么不同。例如,如果引入了新的依赖解析 API,插件必须要么放弃对旧版 Gradle 的支持,要么组织其代码以在兼容版本上条件性地执行新的代码路径。

向前兼容性

主要的兼容性问题在于外部 kotlin-gradle-plugin 版本与 Gradle 附带的 kotlin-stdlib 版本之间。更广泛地说,这适用于任何传递性依赖于 kotlin-stdlib 及其 Gradle 提供的版本的插件。只要版本兼容,一切都应按预期工作。随着 Kotlin 语言的成熟,这个问题将减少。

Kotlin 编译器参数

应用 kotlin-dsl 插件的项目中,以下 Kotlin 编译器参数用于编译 Kotlin DSL 脚本以及 Kotlin 源文件和脚本

-java-parameters

生成 Java >= 1.8 方法参数反射的元数据。有关更多信息,请参阅 Kotlin 文档中的Kotlin/JVM 编译器选项

-Xjvm-default=all

使 Kotlin 接口的所有非抽象成员成为实现它们的 Java 类的默认成员。这是为了使以 Kotlin 编写的插件与 Java 和 Groovy 更好地互操作。有关更多信息,请参阅 Kotlin 文档中的接口中的默认方法

-Xsam-conversions=class

将 SAM(单一抽象方法)转换的实现策略设置为始终生成匿名类,而不是使用 invokedynamic JVM 指令。这是为了更好地支持配置缓存和增量构建。有关更多信息,请参阅 Kotlin issue tracker 中的KT-44912

-Xjsr305=strict

设置 Kotlin 的 Java 互操作性以严格遵循 JSR-305 注解,从而提高 null 安全性。有关更多信息,请参阅 Kotlin 文档中的从 Kotlin 调用 Java 代码

互操作性

在构建逻辑中混合使用语言时,你可能需要跨越语言边界。一个极端的例子是使用 Java、Groovy 和 Kotlin 实现的任务和插件的构建,同时还使用 Kotlin DSL 和 Groovy DSL 构建脚本。

Kotlin 设计时就考虑到了 Java 互操作性。现有 Java 代码可以自然地从 Kotlin 调用,Kotlin 代码也可以相当流畅地从 Java 使用。
— Kotlin 参考文档

从 Kotlin 调用 Java从 Java 调用 Kotlin 在 Kotlin 参考文档中都有很好的涵盖。

这主要也适用于与 Groovy 代码的互操作性。此外,Kotlin DSL 提供了几种方法来选择 Groovy 语义,我们将在下面介绍。

静态扩展

Groovy 和 Kotlin 语言都支持通过Groovy Extension modulesKotlin extensions 扩展现有类。

要从 Groovy 调用 Kotlin 扩展函数,请将其作为静态函数调用,并将接收者作为第一个参数传递

build.gradle
TheTargetTypeKt.kotlinExtensionFunction(receiver, "parameters", 42, aReference)

Kotlin 扩展函数是包级函数。你可以在 Kotlin 参考文档的包级函数部分了解如何查找声明给定 Kotlin 扩展的类型的名称。

要从 Kotlin 调用 Groovy 扩展方法,同样的方法适用:将其作为静态函数调用,并将接收者作为第一个参数传递

build.gradle.kts
TheTargetTypeGroovyExtension.groovyExtensionMethod(receiver, "parameters", 42, aReference)

命名参数和默认参数

Groovy 和 Kotlin 语言都支持命名函数参数和默认参数,尽管它们的实现方式非常不同。Kotlin 对两者都有完善的支持,如 Kotlin 语言参考中的命名参数默认参数所述。Groovy 基于 Map<String, ?> 参数以非类型安全的方式实现命名参数,这意味着它们不能与默认参数结合使用。换句话说,对于任何给定的方法,在 Groovy 中你只能使用其中一种。

从 Groovy 调用 Kotlin

要从 Groovy 调用带有命名参数的 Kotlin 函数,只需使用带位置参数的普通方法调用

build.gradle
kotlinFunction("value1", "value2", 42)

无法通过参数名称提供值。

要从 Groovy 调用带有默认参数的 Kotlin 函数,请始终为所有函数参数传递值。

从 Kotlin 调用 Groovy

要从 Kotlin 调用带有命名参数的 Groovy 函数,你需要传递一个 Map<String, ?>,如本例所示

build.gradle.kts
groovyNamedArgumentTakingMethod(mapOf(
    "parameterName" to "value",
    "other" to 42,
    "and" to aReference))

要从 Kotlin 调用带有默认参数的 Groovy 函数,请始终为所有参数传递值。

从 Kotlin 调用 Groovy closures

有时你可能需要从 Kotlin 代码调用接受Closure 参数的 Groovy 方法。例如,一些用 Groovy 编写的第三方插件期望 closure 参数。

用任何语言编写的 Gradle 插件都应优先使用 Action<T> 类型代替 closures。Groovy closures 和 Kotlin lambdas 会自动映射到该类型的参数。

为了在保留 Kotlin 强类型的同时提供构建 closures 的方法,存在两个帮助方法

  • closureOf<T> {}

  • delegateClosureOf<T> {}

这两种方法在不同情况下都有用,并取决于你将 Closure 实例传递给哪个方法。

一些插件期望简单的 closures,就像Bintray 插件一样

build.gradle.kts
bintray {
    pkg(closureOf<PackageConfig> {
        // Config for the package here
    })
}

在其他情况下,例如使用Gretty 插件配置 farms 时,插件期望 delegate closure

build.gradle.kts
farms {
    farm("OldCoreWar", delegateClosureOf<FarmExtension> {
        // Config for the war here
    })
}

有时从源代码无法很好地判断使用哪个版本。通常,如果使用 closureOf<T> {} 遇到 NullPointerException,使用 delegateClosureOf<T> {} 将解决问题。

这两个实用函数对于配置 closures很有用,但某些插件可能期望 Groovy closures 用于其他目的。KotlinClosure0KotlinClosure2 类型允许更灵活地将 Kotlin 函数适配到 Groovy closures

build.gradle.kts
somePlugin {

    // Adapt parameter-less function
    takingParameterLessClosure(KotlinClosure0({
        "result"
    }))

    // Adapt unary function
    takingUnaryClosure(KotlinClosure1<String, String>({
        "result from single parameter $this"
    }))

    // Adapt binary function
    takingBinaryClosure(KotlinClosure2<String, String, String>({ a, b ->
        "result from parameters $a and $b"
    }))
}

Kotlin DSL Groovy Builder

如果某些插件大量使用了Groovy 元编程,那么从 Kotlin、Java 或任何静态编译语言中使用它可能会非常麻烦。

Kotlin DSL 提供了一个 withGroovyBuilder {} 实用扩展,它将 Groovy 元编程语义附加到类型为 Any 的对象。

以下示例演示了该方法在对象 target 上的一些特性

build.gradle.kts
target.withGroovyBuilder {                                          (1)

    // GroovyObject methods available                               (2)
    if (hasProperty("foo")) { /*...*/ }
    val foo = getProperty("foo")
    setProperty("foo", "bar")
    invokeMethod("name", arrayOf("parameters", 42, aReference))

    // Kotlin DSL utilities
    "name"("parameters", 42, aReference)                            (3)
        "blockName" {                                               (4)
            // Same Groovy Builder semantics on `blockName`
        }
    "another"("name" to "example", "url" to "https://example.com/") (5)
}
1 接收者是一个GroovyObject 并提供了 Kotlin 助手
2 GroovyObject API 可用
3 调用 methodName 方法,传递一些参数
4 配置 blockName 属性,映射到一个接受方法调用的 Closure
5 调用接受命名参数的 another 方法,映射到一个接受方法调用的 Groovy 命名参数 Map<String, ?>

使用 Groovy 脚本

处理假定使用 Groovy DSL 构建脚本的问题插件时,另一种选择是在从主 Kotlin DSL 构建脚本应用的 Groovy DSL 构建脚本中配置它们

dynamic-groovy-plugin-configuration.gradle
native {    (1)
    dynamic {
        groovy as Usual
    }
}
build.gradle.kts
plugins {
    id("dynamic-groovy-plugin") version "1.0"   (2)
}
apply(from = "dynamic-groovy-plugin-configuration.gradle")  (3)
1 Groovy 脚本使用动态 Groovy 配置插件
2 Kotlin 构建脚本请求并应用插件
3 Kotlin 构建脚本应用 Groovy 脚本

故障排除

IDE 支持由两个组件提供

  1. Kotlin 插件(供 IntelliJ IDEA/Android Studio 使用)。

  2. Gradle。

支持级别因各个版本而异。

如果遇到问题,首先从命令行运行 ./gradlew tasks 以确定问题是否特定于 IDE。如果问题在命令行上仍然存在,则很可能源于构建本身,而不是 IDE 集成。

但是,如果构建在命令行上成功运行,但你的脚本编辑器报告错误,请尝试重新启动 IDE 并使其缓存失效。

如果问题仍然存在,并且你怀疑 Kotlin DSL 脚本编辑器有问题,请尝试以下操作

  • 运行 ./gradlew tasks 以收集更多详细信息。

  • 检查以下位置的日志

    • macOS 上的 $HOME/Library/Logs/gradle-kotlin-dsl

    • Linux 上的 $HOME/.gradle-kotlin-dsl/log

    • Windows 上的 $HOME/AppData/Local/gradle-kotlin-dsl/log

  • Gradle 问题跟踪器上报告问题,包含尽可能多的详细信息。

从 5.1 版本开始,日志目录会自动清理。日志会定期检查(最多每 24 小时一次),如果文件在 7 天内未使用,则会被删除。

如果这仍无法查明问题,你可以在 IDE 中启用 org.gradle.kotlin.dsl.logging.tapi 系统属性。这会导致 Gradle Daemon 在其日志文件(位于 $HOME/.gradle/daemon)中记录更多详细信息。

在 IntelliJ IDEA 中,通过导航到 Help > Edit Custom VM Options…​ 并添加 -Dorg.gradle.kotlin.dsl.logging.tapi=true 来启用此属性。

对于 Kotlin DSL 脚本编辑器之外的 IDE 问题,请在相应的 IDE 问题跟踪器中创建问题

最后,如果你遇到 Gradle 本身或 Kotlin DSL 的问题,请在Gradle 问题跟踪器上创建问题。

限制

  • Kotlin DSL 在首次使用时已知比 Groovy DSL 慢,例如在干净检出或临时持续集成代理上。更改 buildSrc 目录中的内容也会产生影响,因为它会使构建脚本缓存失效。主要原因是 Kotlin DSL 的脚本编译速度较慢。

  • 在 IntelliJ IDEA 中,你必须从 Gradle 模型导入项目,才能为你的 Kotlin DSL 构建脚本获得内容辅助和重构支持。

  • Kotlin DSL 脚本编译避免功能存在已知问题。如果遇到问题,可以通过设置 org.gradle.kotlin.dsl.scriptCompilationAvoidance 系统属性为 false 来禁用它。

  • Kotlin DSL 将不支持 model {} 块,它是已终止的 Gradle 软件模型的一部分。

如果你遇到麻烦或发现疑似 bug,请在Gradle 问题跟踪器中报告问题。