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 支持

IntelliJ IDEA 和 Android Studio 完全支持 Kotlin DSL。虽然其他 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 文件(例如,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 使用 相应 API 文档 中列出的包(但不包括其子包)中的类型扩展了公共 Gradle API。

编译警告

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

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

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

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

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

类型安全模型访问器

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

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

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

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

Kotlin DSL 当前提供了各种类型的类型安全模型访问器,每种访问器都针对不同的作用域。

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

类型安全模型访问器 示例

依赖和构件配置

implementationruntimeOnly(由 Java 插件贡献)

项目扩展和约定,以及它们的扩展

sourceSets

dependenciesrepositories 容器及其扩展的扩展

testImplementation(由 Java 插件贡献)、mavenCentral

tasksconfigurations 容器中的元素

compileJava(由 Java 插件贡献)、test

项目扩展容器中的元素

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

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

类型安全模型访问器 示例

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

pluginManagementdependencyResolutionManagement

初始化脚本和脚本插件没有类型安全模型访问器。 这些限制将在未来的 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 Library 插件 贡献的 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。 为此,您需要知道已配置模型元素的名称和/或类型。 我们现在将向您展示如何通过详细检查脚本来发现这些信息。

构件配置

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

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 上类型为 SourceSetContainer 的 Gradle 扩展,而 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 来发现任何容器中存在的元素。

作为最后的手段,您也可以查看插件的源代码以了解它的作用。

Task

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

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

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

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

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

请注意,必须显式指定 Task 的类型。 如果您不这样做,脚本将无法编译,因为推断的类型将是 Task,而不是 Test,并且 testLogging 属性特定于 Test Task 类型。

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

您可以通过运行 ./gradlew tasks 来发现可用的 Task。

要查找给定 Task 的类型,请运行 ./gradlew help --task <taskName>,如此处演示

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

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

关于约定

一些 Gradle 核心插件借助所谓的约定对象公开可配置性。 这些约定对象的作用与扩展类似,并且现在已被扩展取代。 约定已弃用,因此在编写新插件时请避免使用约定对象。

如上所述,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() 方法配置现有 Task,以及使用 register() 方法创建新 Task

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 的 Task 类型引用
2 注册一个新的未类型化的名为 myTask1 的 Task
3 获取对名为 compileJava 的现有 Task 的 JavaCompile 类型引用
4 注册一个新的名为 myCopy1Copy 类型 Task
5 获取对名为 assemble 的现有(未类型化)Task 的引用并配置它 — 使用此语法,您只能配置 Task 上可用的属性和方法
6 注册一个新的未类型化的名为 myTask2 的 Task 并配置它 — 在这种情况下,您只能配置 Task 上可用的属性和方法
7 获取对名为 test 的现有 Task 的 Test 类型引用并配置它 — 在这种情况下,您可以访问指定类型的属性和方法
8 注册一个新的名为 myCopy2Copy 类型 Task 并配置它
以上示例依赖于配置避免 API。 如果您需要或想要急切地配置或注册容器元素,只需将 named() 替换为 getByName(),并将 register() 替换为 create()

使用 Kotlin 委托属性

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

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

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 Task 的引用,而不是 Task 路径
以上示例依赖于配置避免 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 脚本和初始化脚本中也适用,除了您分别使用 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 脚本和初始化脚本。

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

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

额外属性不仅限于项目。 例如,Task 扩展了 ExtensionAware,因此您也可以将额外属性附加到 Task。

这里有一个示例,它在 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 任务

如果你乐于使用预先配置而不是配置避免 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 将假定我们想要从 *task* 中读取一个额外属性

使用 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 项目包含的构建Gradle 插件

该插件通过执行以下操作来实现这一点

  • 应用 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 工具链。默认情况下,代码将以 Java 8 为目标。你可以通过定义项目要使用的 Java 工具链来更改它

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 类路径可见。

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

向后兼容性

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

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

向前兼容性

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

Kotlin 编译器参数

以下 Kotlin 编译器参数用于编译 Kotlin DSL 脚本,以及应用了 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 问题跟踪器中的 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 扩展模块Kotlin 扩展来扩展现有类。

要从 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 闭包

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

用任何语言编写的 Gradle 插件都应该首选 Action<T> 类型来代替闭包。Groovy 闭包和 Kotlin lambda 会自动映射到该类型的参数。

为了提供一种在保留 Kotlin 的强类型的同时构造闭包的方法,存在两种辅助方法

  • closureOf<T> {}

  • delegateClosureOf<T> {}

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

一些插件期望简单的闭包,例如 Bintray 插件

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

在其他情况下,例如使用 Gretty Plugin 配置 farm 时,该插件期望委托闭包

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

有时没有很好的方法从源代码中判断要使用哪个版本。通常,如果你使用 closureOf<T> {} 得到 NullPointerException,则使用 delegateClosureOf<T> {} 将解决问题。

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

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 构建器

如果某些插件大量使用 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 问题跟踪器 中报告该问题。