Gradle 的 Kotlin DSL 提供傳統 Groovy DSL 的替代語法,並在支援的 IDE 中提供增強的編輯體驗,具備優異的內容協助、重構、文件等功能。本章提供 Kotlin DSL 主要建構的詳細資料,以及如何使用它與 Gradle API 互動。

如果您有興趣將現有的 Gradle 建置移轉至 Kotlin DSL,請務必查看專屬的 移轉部分

先備條件

  • 已知嵌入式 Kotlin 編譯器可在 x86-64 架構的 Linux、macOS、Windows、Cygwin、FreeBSD 和 Solaris 上運作。

  • 具備 Kotlin 語法和基本語言功能的知識非常有幫助。Kotlin 參考文件Kotlin Koans 將協助您學習基礎知識。

  • 使用 plugins {} 區塊宣告 Gradle 外掛可顯著改善編輯體驗,強烈建議使用。

IDE 支援

IntelliJ IDEA 和 Android Studio 完全支援 Kotlin DSL。其他 IDE 尚未提供有助於編輯 Kotlin DSL 檔案的工具,但您仍可以匯入基於 Kotlin-DSL 的建置,並照常使用。

表 1. IDE 支援矩陣
建置匯入 語法突顯 1 語意編輯器 2

IntelliJ IDEA

Android Studio

Eclipse IDE

CLion

Apache NetBeans

Visual Studio Code (LSP)

Visual Studio

1 Kotlin 語法 突顯 Gradle Kotlin DSL 指令碼
2 程式碼 完成、 導覽至 來源、 文件、 重構等…​ Gradle Kotlin DSL 指令碼

如限制事項中所述,您必須 從 Gradle 模型匯入專案,才能在 IntelliJ IDEA 中取得 Kotlin DSL 指令碼的內容協助和重構工具。

建置設定時間過長可能會影響 IDE 的回應速度,請參閱 效能區段,以協助解決此類問題。

自動建置匯入相對於自動重新載入指令碼相依性

IntelliJ IDEA 和 Android Studio(源自 IntelliJ IDEA)都會偵測您對建置邏輯所做的變更,並提供兩個建議

  1. 再次匯入整個建置

    IntelliJ IDEA
    IntelliJ IDEA
  2. 編輯建置指令碼時重新載入指令碼相依性

    Reload script dependencies

建議您停用自動建置匯入,但啟用自動重新載入指令碼相依性。這樣一來,您在編輯 Gradle 指令碼時可以及早收到回饋,並控制與 IDE 同步整個建置設定的時間。

疑難排解

IDE 支援由兩個元件提供

  • IntelliJ IDEA/Android Studio 使用的 Kotlin 外掛程式

  • Gradle

支援層級會根據各元件的版本而有所不同。

如果您遇到問題,應先嘗試從命令列執行 ./gradlew tasks,以查看您的問題是否僅限於 IDE。如果您在命令列中遇到相同的問題,則問題出在建置,而非 IDE 整合。

如果您可以在命令列中成功執行建置,但您的指令碼編輯器抱怨,則您應該嘗試重新啟動 IDE 並使快取失效。

如果上述方法不起作用,而且您懷疑 Kotlin DSL 腳本編輯器有問題,您可以

  • 執行 ./gradle tasks 以取得更多詳細資料

  • 查看下列其中一個位置的記錄

    • Mac OS X 上的 $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 中,可以透過開啟 說明 > 編輯自訂 VM 選項…​,然後新增 -Dorg.gradle.kotlin.dsl.logging.tapi=true 來執行此操作。

對於 Kotlin DSL 腳本編輯器以外的 IDE 問題,請在對應的 IDE 問題追蹤器中開啟問題

最後,如果您遇到 Gradle 本身或 Kotlin DSL 的問題,請在 Gradle 問題追蹤器 中開啟問題。

Kotlin DSL 腳本

就像基於 Groovy 的等效項一樣,Kotlin DSL 是在 Gradle 的 Java API 上實作的。您可以在 Kotlin DSL 腳本中讀取的所有內容都是由 Gradle 編譯並執行的 Kotlin 程式碼。您在建置腳本中使用的許多物件、函式和屬性都來自 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 命名設定腳本(或任何由 Gradle Settings 物件支援的腳本)—這包括從設定腳本套用的腳本外掛

  • 根據範本 *.init.gradle.kts 或單純的 init.gradle.kts 命名 初始化腳本

這是為了讓 IDE 知道「支援」腳本的物件類型,無論是 ProjectSettingsGradle

內含匯入

所有 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

您可以設定 org.gradle.kotlin.dsl.allWarningsAsErrors Gradle 屬性為 true,將建置組態為在腳本編譯期間發出任何警告時失敗

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

類型安全的模型存取器

Groovy DSL 允許您按名稱參照建置模型的許多元素,即使它們是在執行階段定義的。例如,已命名組態、已命名來源集等。例如,您可以透過 configurations.implementation 取得 implementation 組態。

Kotlin DSL 以類型安全的模型存取器取代此類動態解析,這些存取器可與外掛提供的模型元素搭配使用。

了解何時可以使用類型安全的模型存取器

Kotlin DSL 目前支援外掛提供的下列類型安全模型存取器

  • 相依關係和成品組態(例如 Java 外掛提供的 implementationruntimeOnly

  • 專案延伸和慣例(例如 sourceSets

  • dependenciesrepositories 容器的延伸

  • tasksconfigurations 容器中的元素

  • 專案延伸容器 中的元素(例如 Java 外掛提供的來源集,已新增至 sourceSets 容器)

  • 上述各項的擴充

只有主專案建置指令碼和預編譯專案指令碼外掛具有類型安全的模型存取器。初始化指令碼、設定指令碼、指令碼外掛則沒有。這些限制將在未來的 Gradle 版本中移除。

可用的類型安全模型存取器集合會在評估指令碼主體之前計算,緊接在 plugins {} 區塊之後。在那之後新增的任何模型元素都不會與類型安全模型存取器一起運作。例如,這包括您可能在自己的建置指令碼中定義的任何組態。不過,這種方法確實表示您可以對由父專案套用的外掛所提供的任何模型元素使用類型安全存取器。

下列專案建置指令碼示範如何使用類型安全存取器存取各種組態、擴充和其他元素

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>,並提供基礎工作的延遲參考和延遲組態。以下是一些說明配置避免適用的情況的範例

tasks.test {
    // lazy configuration
}

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

testProvider {
    // lazy configuration
}

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

對於 tasks 以外的所有其他容器,元素的存取器類型為 NamedDomainObjectProvider<T>,並提供相同的行為。

了解在沒有類型安全模型存取器時該怎麼做

考慮上述顯示如何使用類型安全存取器的範例建置指令碼。下列範例完全相同,只不過它使用 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 實作的 Binary Gradle 外掛中使用類型安全存取器。

如果您找不到類型安全存取器,請改用對應類型的正常 API。為此,您需要知道已設定的模型元素的名稱和/或類型。我們現在將展示如何透過詳細檢視上述指令碼來找出這些資訊。

成品設定

以下範例示範如何不使用類型存取器來參照和設定成品設定

範例 3. 成品設定
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 無法協助您找出可用的設定,但您可以查看對應的外掛文件或執行 gradle 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

您可以透過查看已套用外掛的文件或執行 gradle kotlinDslAccessorsReport 來找出有哪些可用的擴充和慣例,這會列印存取所有已套用外掛所提供的模型元素所需的 Kotlin 程式碼。報告會提供名稱和類型。最後,您也可以查看外掛的原始程式碼,但在大多數情況下沒有必要這麼做。

請注意,如果您只需要參照擴充或慣例而不設定它,或者您想執行單行設定,也可以使用 the<T>() 函式,如下所示

the<SourceSetContainer>()["main"].srcDir("src/core/java")

上述程式碼片段也示範了設定容器專案擴充元素的方法之一。

專案擴充容器中的元素

基於容器的專案擴充功能,例如 SourceSetContainer,也允許您設定它們所包含的元素。在我們的範例建置指令碼中,我們想要設定來源組容器中的名為 main 的來源組,我們可以使用 named() 方法來取代存取器,如下所示

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

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

容器中所有元素都有名稱,因此您可以在所有此類情況中使用此技術。

至於專案擴充功能和慣例本身,您可以透過查看套用外掛程式的文件或執行 gradle kotlinDslAccessorsReport 來找出任何容器中有哪些元素。最後,您可能可以查看外掛程式的原始碼來找出它執行的動作,但在大多數情況下,這並非必要。

工作

工作並非透過基於容器的專案擴充功能管理,但它們是容器的一部分,其行為方式類似。這表示您可以像設定來源組一樣設定工作,如您在這個範例中所見

範例 6. 工作
build.gradle.kts
apply(plugin = "java-library")

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

我們使用 Gradle API 根據名稱和類型來參照工作,而不是使用存取器。請注意,必須明確指定工作的類型,否則指令碼將無法編譯,因為推論的類型將會是 Task,而不是 Test,而 testLogging 屬性是特定於 Test 工作類型的。不過,如果您只需要設定屬性或呼叫所有工作共用的方法(即它們在 Task 介面上宣告),則可以省略類型。

可以透過執行 gradle tasks 來找出有哪些工作可用。然後,您可以透過執行 gradle help --task <taskName> 來找出特定工作的類型,如這裡所示

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

請注意,IDE 可以協助您處理所需的匯入,因此您只需要類型的簡單名稱,即不需要套件名稱部分。在此情況中,不需要匯入 Test 工作類型,因為它是 Gradle API 的一部分,因此會 隱式匯入

關於慣例

部分 Gradle 核心外掛程式會透過所謂的慣例物件來公開設定功能。這些物件用途類似於(現在已被)擴充功能取代。慣例已過時。請避免在撰寫新的外掛程式時使用慣例物件。

如上所述,Kotlin DSL 僅提供 Project 上慣例物件的存取器。在某些情況下,您需要與使用其他類型上慣例物件的 Gradle 外掛程式進行互動。Kotlin DSL 提供 withConvention(T::class) {} 擴充功能來執行此動作

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

此技術主要適用於語言外掛新增的來源集,但尚未移轉至擴充功能。

多專案建置

與單一專案建置一樣,您應嘗試在多專案建置中使用 plugins {} 區塊,以便使用類型安全存取器。多專案建置的另一個考量是,您將無法在根建置指令碼中設定子專案,或在專案之間使用其他形式的跨設定時,使用類型安全存取器。我們將在以下各節中更詳細地討論這兩個主題。

套用外掛

您可以在套用外掛的子專案中宣告外掛,但我們建議您也在根專案建置指令碼中宣告外掛。這有助於在建置中的專案之間保持外掛版本的一致性。此方法也能提升建置效能。

使用 Gradle 外掛 章節說明如何使用版本在根專案建置指令碼中宣告外掛,然後將其套用至適當的子專案建置指令碼。以下是使用三個子專案和三個外掛的範例。請注意根建置指令碼只宣告社群外掛,因為 Java 函式庫外掛已繫結至您使用的 Gradle 版本

settings.gradle.kts
rootProject.name = "multi-project-build"
include("domain", "infra", "http")
build.gradle.kts
plugins {
    id("com.github.johnrengelman.shadow") version "7.1.2" apply false
    id("io.ratpack.ratpack-java") version "1.8.2" apply false
}
domain/build.gradle.kts
plugins {
    `java-library`
}

dependencies {
    api("javax.measure:unit-api:1.0")
    implementation("tec.units:unit-ri:1.0.3")
}
infra/build.gradle.kts
plugins {
    `java-library`
    id("com.github.johnrengelman.shadow")
}

shadow {
    applicationDistribution.from("src/dist")
}

tasks.shadowJar {
    minimize()
}
http/build.gradle.kts
plugins {
    java
    id("io.ratpack.ratpack-java")
}

dependencies {
    implementation(project(":domain"))
    implementation(project(":infra"))
    implementation(ratpack.dependency("dropwizard-metrics"))
}

application {
    mainClass = "example.App"
}

ratpack.baseDir = file("src/ratpack/baseDir")

如果您的建置需要在 Gradle 外掛入口網站之外的其他外掛存放庫,您應在 settings.gradle.kts 檔案中的 pluginManagement {} 區塊中宣告,如下所示

settings.gradle.kts
pluginManagement {
    repositories {
        mavenCentral()
        gradlePluginPortal()
    }
}

Gradle 外掛入口網站 以外的來源取得的外掛,只有在使用其 外掛標記成品 發佈時,才能透過 plugins {} 區塊宣告。

撰寫本文時,google() 存放庫中所有 Android 外掛 for Gradle 版本(至 3.2.0)都缺少外掛標記成品。

如果缺少這些成品,您便無法使用 plugins {} 區塊。您必須改用根專案建置指令碼中的 buildscript {} 區塊宣告外掛相依性。以下是 Android 外掛的範例

settings.gradle.kts
include("lib", "app")
build.gradle.kts
buildscript {
    repositories {
        google()
        gradlePluginPortal()
    }
    dependencies {
        classpath("com.android.tools.build:gradle:7.3.0")
    }
}
lib/build.gradle.kts
plugins {
    id("com.android.library")
}

android {
    // ...
}
app/build.gradle.kts
plugins {
    id("com.android.application")
}

android {
    // ...
}

此技巧與 Android Studio 在建立新建置時產生的結果並無不同。主要差異在於上述範例中的子專案建置指令碼使用 plugins {} 區塊宣告其外掛。這表示您可以使用類型安全的存取器來存取它們所提供的模型元素。

請注意,如果您想要將此類外掛套用至多專案建置的根目錄建置指令碼(而並非僅套用至其子專案),或套用至單一專案建置,則無法使用此技巧。在這些情況下,您需要使用我們在 另一個區段 中詳細說明的不同方法。

跨專案設定

跨專案設定 是一種機制,讓您可以從另一個專案的建置指令碼設定專案。常見的範例是在根目錄建置指令碼中設定子專案。

採取這種方法表示您將無法使用類型安全的存取器來存取外掛所提供的模型元素。您必須仰賴字串文字和標準 Gradle API。

舉例來說,我們修改 Java/Ratpack 範例建置,以從根目錄建置指令碼完全設定其子專案

範例 11. 跨專案設定
settings.gradle.kts
rootProject.name = "multi-project-build"
include("domain", "infra", "http")
build.gradle.kts
import com.github.jengelman.gradle.plugins.shadow.ShadowExtension
import com.github.jengelman.gradle.plugins.shadow.tasks.ShadowJar
import ratpack.gradle.RatpackExtension

plugins {
    id("com.github.johnrengelman.shadow") version "7.1.2" apply false
    id("io.ratpack.ratpack-java") version "1.8.2" apply false
}

project(":domain") {
    apply(plugin = "java-library")
    repositories { mavenCentral() }
    dependencies {
        "api"("javax.measure:unit-api:1.0")
        "implementation"("tec.units:unit-ri:1.0.3")
    }
}

project(":infra") {
    apply(plugin = "java-library")
    apply(plugin = "com.github.johnrengelman.shadow")
    configure<ShadowExtension> {
        applicationDistribution.from("src/dist")
    }
    tasks.named<ShadowJar>("shadowJar") {
        minimize()
    }
}

project(":http") {
    apply(plugin = "java")
    apply(plugin = "io.ratpack.ratpack-java")
    repositories { mavenCentral() }
    val ratpack = the<RatpackExtension>()
    dependencies {
        "implementation"(project(":domain"))
        "implementation"(project(":infra"))
        "implementation"(ratpack.dependency("dropwizard-metrics"))
        "runtimeOnly"("org.slf4j:slf4j-simple:1.7.25")
    }
    configure<JavaApplication> {
        mainClass = "example.App"
    }
    ratpack.baseDir = file("src/ratpack/baseDir")
}

請注意,我們如何使用 apply() 方法套用外掛,因為 plugins {} 區塊無法在此背景下運作。我們也使用標準 API 而不是類型安全的存取器來設定工作、擴充功能和慣例,這是我們在 其他地方 討論過的方法。

當您無法使用 plugins {} 區塊時

Gradle 外掛入口網站 以外的來源擷取的外掛可能無法與 plugins {} 區塊搭配使用。這取決於它們的發布方式,特別是它們是否已使用必要的 外掛標記人工製品 發布。

例如,Android Plugin for Gradle 未發佈到 Gradle Plugin Portal,而且至少到該外掛程式版本 3.2.0 為止,解析特定外掛程式識別碼的必要元資料並未發佈到 Google 儲存庫。

如果您的組建是多專案組建,而且您不需要將此類外掛程式套用至您的根目錄專案,那麼您可以使用上方所述的技術來解決此問題。對於任何其他情況,請繼續閱讀。

發佈外掛程式時,請使用 Gradle 內建的Gradle Plugin Development Plugin

它會自動發佈元資料,讓您的外掛程式可以使用 plugins {} 區塊。

我們將在本節說明如何將 Android Plugin 套用至單一專案組建或多專案組建的根目錄專案。目標是指示您的組建如何將 com.android.application 外掛程式識別碼對應至可解析的成品。這分為兩個步驟

  • 將外掛程式儲存庫新增至組建的設定指令碼

  • 將外掛程式 ID 對應至對應的成品座標

您可以透過在組建的設定指令碼中設定 pluginManagement {} 區塊來完成這兩個步驟。為了示範,以下範例將 google() 儲存庫(Android 外掛程式發佈的位置)新增至儲存庫搜尋清單,並使用 resolutionStrategy {} 區塊將 com.android.application 外掛程式 ID 對應至 google() 儲存庫中可用的 com.android.tools.build:gradle:<version> 成品

settings.gradle.kts
pluginManagement {
    repositories {
        google()
        gradlePluginPortal()
    }
    resolutionStrategy {
        eachPlugin {
            if(requested.id.namespace == "com.android") {
                useModule("com.android.tools.build:gradle:${requested.version}")
            }
        }
    }
}
build.gradle.kts
plugins {
    id("com.android.application") version "7.3.0"
}

android {
    // ...
}

事實上,上述範例適用於由指定模組提供的全部 com.android.* 外掛程式。這是因為封裝模組包含外掛程式 ID 對應至哪個外掛程式實作類別的詳細資料,使用撰寫自訂外掛程式章節中所述的屬性檔案機制。

請參閱 Gradle 使用者手冊的外掛程式管理章節,以取得有關 pluginManagement {} 區塊及其用途的更多資訊。

使用容器物件

Gradle 組建模型大量使用容器物件(或僅「容器」)。例如,configurationstasks 都是容器物件,分別包含 ConfigurationTask 物件。社群外掛程式也會提供容器,例如 Android Plugin 所提供的 android.buildTypes 容器。

Kotlin DSL 提供多種方式讓組建作者與容器互動。接下來,我們將使用 tasks 容器為例,來檢視每種方式。

請注意,如果您正在設定受支援容器上的現有元素,您可以利用另一章節中所述的類型安全存取器。該章節也說明了哪些容器支援類型安全存取器。

使用容器 API

Gradle 中的所有容器都實作 NamedDomainObjectContainer<DomainObjectType>。其中一些容器可以包含不同類型的物件,並實作 PolymorphicDomainObjectContainer<BaseType>。與容器互動的最簡單方式是透過這些介面。

以下範例示範如何使用 named() 方法來設定現有的工作,以及使用 register() 方法來建立新的工作。

範例 13. 使用容器 API
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 取得現有工作 checkTask 類型參考
2 註冊一個名為 myTask1 的新未指定類型工作
3 取得現有工作 compileJavaJavaCompile 類型參考
4 註冊一個名為 myCopy1Copy 類型新工作
5 取得現有(未指定類型)工作 assemble 的參考並設定它 — 使用此語法,您只能設定在 Task 上可用的屬性和方法
6 註冊一個名為 myTask2 的新未指定類型工作並設定它 — 在此情況下,您只能設定在 Task 上可用的屬性和方法
7 取得現有工作 testTest 類型參考並設定它 — 在此情況下,您可以存取指定類型的屬性和方法
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 委派屬性的組合

範例 15. 容器範圍
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,建置就不會因為使用 myNullableProperty 值而失敗(套用標準 Kotlin null 安全規則

相同的方法適用於設定和初始化指令碼,只不過您分別使用 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 myProperty: String by extra  (3)
val myNullableProperty: String? by extra  (4)
1 在目前內容(此案例中的專案)中建立一個名為 myNewProperty 的新額外屬性,並使用值 "initial value" 初始化它,該值也決定屬性的類型
2 建立一個新的額外屬性,其初始值由提供的 lambda 計算
3 將現有額外屬性從目前內容(本例中的專案)繫結到 myProperty 參照
4 與前一行相同,但允許屬性為空值

此方法適用於所有 Gradle 腳本:專案建置腳本、腳本外掛、設定腳本和初始化腳本。

您也可以使用下列語法從子專案存取根專案的額外屬性

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.destination)
    }
}
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>("archiveTestReports") {
    archiveAppendix = testReportType  (2)
    from(test.get().reports.html.destination)
}
1 test 工作上建立並初始化額外屬性,將其繫結到「全域」屬性
2 使用「全域」屬性初始化 archiveTestReports 工作

還有一個我們應該說明的額外屬性語法,將 extra 視為一個映射。我們建議不要使用此語法,因為您會失去 Kotlin 型別檢查的優點,而且它會讓 IDE 無法提供最大的支援。不過,它比委派屬性語法簡潔,如果您只需要設定額外屬性的值而不會在稍後參照它,就可以合理地使用它。

以下是一個簡單範例,說明如何使用映射語法設定和讀取額外屬性

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

tasks.create("myTask") {
    doLast {
        println("Property: ${project.extra["myNewProperty"]}")  (2)
    }
}
1 建立一個新的專案額外屬性,稱為 myNewProperty,並設定其值
2 讀取我們建立的專案額外屬性值 — 請注意 project. 限定詞在 extra[…​] 上,否則 Gradle 會假設我們要從工作讀取額外屬性

Kotlin 延遲屬性指定

Gradle 的 Kotlin DSL 支援使用 = 算子進行延遲屬性指定。當使用延遲屬性時,延遲屬性指定可減少 Kotlin DSL 的冗長性。它適用於公開視為 final(沒有設定器)且類型為 PropertyConfigurableFileCollection 的屬性。由於屬性必須為 final,因此我們的建議是不要為具有延遲類型的屬性實作自訂設定器,並盡可能透過抽象取得器實作此類屬性。

使用 = 算子是呼叫 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 編譯器,確保您的建置邏輯和這些腳本之間的一致性

  • 啟用 預編譯指令碼外掛 的支援。

避免為 kotlin-dsl 外掛指定版本

每個 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)
    }
}
buildSrc/src/main/groovy/myproject.java-conventions.gradle
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,而外掛想要使用該 API,那麼它們需要放棄對舊版 Gradle 版本的支持,或需要對其程式碼進行一些巧妙的組織,才能僅在新版本上執行新的程式碼路徑。

向前相容性

最大的問題是外部 kotlin-gradle-plugin 版本與 Gradle 附帶的 kotlin-stdlib 版本之間的相容性。更一般來說,在任何遞移依賴於 kotlin-stdlib 的外掛與其 Gradle 附帶的版本之間。只要組合相容,一切都應該正常運作。隨著語言的成熟,這將不再是一個問題。

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 注解,以提高空值安全性。請參閱 Kotlin 文件中的 從 Kotlin 呼叫 Java 程式碼 以取得更多資訊。

互操作性

在建置邏輯中混合語言時,您可能必須跨越語言界限。一個極端的範例會是一個建置,它使用以 Java、Groovy 和 Kotlin 實作的任務和外掛程式,同時也使用 Kotlin DSL 和 Groovy DSL 建置指令碼。

引述 Kotlin 參考文件

Kotlin 的設計考量了與 Java 的互操作性。現有的 Java 程式碼可以用自然的方式從 Kotlin 呼叫,而 Kotlin 程式碼也可以從 Java 順利使用。

在 Kotlin 參考文件中,從 Kotlin 呼叫 Java從 Java 呼叫 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 函式,只要使用具有位置參數的正常方法呼叫即可。沒有辦法透過引數名稱提供值。

要從 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 程式碼呼叫接受 閉包 引數的 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 外掛程式,外掛程式會預期委派閉包

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 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 腳本

限制

  • 已知 Kotlin DSL 在初次使用時比 Groovy DSL 慢,例如在乾淨的結帳或短暫的持續整合代理程式上。在 buildSrc 目錄中變更某些內容也會產生影響,因為它會使建置腳本快取無效。造成此問題的主要原因是 Kotlin DSL 的腳本編譯較慢。

  • 在 IntelliJ IDEA 中,您必須 從 Gradle 模型匯入專案,才能為 Kotlin DSL 建置腳本取得內容協助和重構支援。

  • 已知 Kotlin DSL 腳本編譯避免有問題。如果您遇到問題,可以透過 設定 org.gradle.kotlin.dsl.scriptCompilationAvoidance 系統屬性為 false 來停用它。

  • Kotlin DSL 不會支援 model {} 區塊,它是 已中止的 Gradle 軟體模型 的一部分。

如果您遇到問題或發現疑似錯誤,請在 Gradle 問題追蹤器 中回報問題。