Gradle遷移指南:從Groovy到KTS

語言: CN / TW / HK

theme: smartblue

Android Gradle 外掛 4.0 支援在 Gradle 構建配置中使用 Kotlin 指令碼 (KTS),用於替代 Groovy(過去在 Gradle 配置檔案中使用的程式語言)。

將來,KTS 會比 Groovy 更適合用於編寫 Gradle 指令碼,因為採用 Kotlin 編寫的程式碼可讀性更高,並且 Kotlin 提供了更好的編譯時檢查和 IDE 支援

雖然與 Groovy 相比,KTS 當前能更好地在 Android Studio 的程式碼編輯器中整合,但採用 KTS 的構建速度往往比採用 Groovy 慢,因此在遷移到 KTS 時應考慮構建效能

常用術語

KTS:指Kotlin指令碼,是Gradle在構建配置檔案中使用的一種 Kotlin 語言形式。Kotlin 指令碼是可從命令列執行的 Kotlin 程式碼。

Kotlin DSL:主要是指 Android Gradle 外掛 Kotlin DSL,有時也指底層 Gradle Kotlin DSL。

檔案命名

  • 用 Groovy 編寫的 Gradle build 檔案使用 .gradle 副檔名
  • 用 Kotlin 編寫的 Gradle build 檔案使用 .gradle.kts 副檔名

遷移思路

Groovy 的語法和 Kotlin 的語法雖然相差不小,但在 Gradle DSL 的設計上,還是儘可能保持了統一性,這顯然也是為了降低大家的學習和遷移成本。正因為如此,儘管我們還是要對兩門語言的一些語法細節進行批量處理,遷移過程實際上並不複雜。

處理字串字面量

主要修改點在於 settings.gradle以及幾個 build.gradle

Groovy 中,單引號引起來的也是字串字面量,因此我們會面對大量這樣的寫法:

groovy include ':app', ':tdc_core', ':tdc_uicompat', ':tdc_utils'

這在 Kotlin 中是不允許的,因此需要想辦法將字串字面量單引號統一改為雙引號,可以使用Android Studio的 全域性正則替換

image.png

  • 匹配框輸入正則表示式 '(.*?[^\])',替換框中填寫 "$1",這裡的 $1 對應於正則表示式當中的第一個元組,如果有多個元組,可以用 $n 來表示,其中 $0 表示匹配到的整個字元
  • 過濾檔案字尾,我們只對 *.gradle 檔案做替換
  • 在檔案字尾後面的漏斗當中選擇 Excepts String literals and Comments,表示我們只匹配程式碼部分
  • 在輸入框後面選擇 .*,藍色高亮表示啟用正則匹配

檢查匹配內容,對匹配錯誤的部分進行修改,點選 Replace All,所有單引號會變成雙引號:

groovy include ":app", ":tdc_core", ":tdc_uicompat", ":tdc_utils"

給方法呼叫加上括號

仍以 settings.gradle為例:

groovy include ":app", ":tdc_core", ":tdc_uicompat", ":tdc_utils"

此處實際是一個方法呼叫,在 Groovy 中只要沒有歧義,就可以把方法呼叫的括號省略,這在 Kotlin 中是不行的,因此需要統一做加括號處理,採用 全域性正則替換 方法:

image.png

  • 匹配框輸入正則表示式 (\w+) (([^={\s]+)(.*)),替換框中填寫 $1($2),其他配置與前面替換引號一樣

檢查匹配內容,對匹配錯誤的部分進行修改,點選 Replace All,所有方法呼叫都加上了括號:

groovy include(":app", ":tdc_core", ":tdc_uicompat", ":tdc_utils")

開始遷移

遷移 settings.gradle

首先,將檔名改為 settings.gradle.kts, 然後 sync。 經過前面兩步操作,settings.gradle內容已經是合法的 Kotlin 程式碼了。

遷移根目錄下的 build.gradle

給檔案增加 .kts字尾,sync之後開始解決報錯:

訪問extra擴充套件

過去我們都是通過 ext訪問 project物件的動態屬性(參考視訊:Project屬性都是哪裡來的?),Groovy的動態特性支援了這一語法,但Kotlin作為一門靜態語言是不支援的。因此想要訪問ext,就需要使用extra擴充套件,或者 getProperties()["ext"],所以:

groovy ext.kotlin_version = "1.4.30"

等價於

kotlin extra["kotlin_version"] = "1.4.30"

接下來就是對 kotlin_version的訪問了,需要將它取出來再使用:

kotlin val kotlin_version: String by extra ... classpath("org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version")

建立任務

參考 clean任務的修改方式:

``` kotlin // 方法一(推薦使用) tasks.register("clean") { delete(rootProject.buildDir) }

// 方法二 task("clean", Delete::class) { delete(rootProject.buildDir) } ``` 在 Groovy 當中 Delete 型別是作為引數通過 Key-Value 的形式傳遞的,Kotlin 當中直接把它當做泛型引數傳入,這樣設計是非常符合 Kotlin 的設計思想。

maven配置

maven語法比較簡單,直接修改為:

kotlin repositories { google() mavenCentral() maven("https://jitpack.io/") maven("http://maven.xxx.com/") { // 信任http協議 isAllowInsecureProtocol = true } }

遷移app模組下的 build.gradle

確保頂部 plugins配置正確,等待IDE建立索引完畢,各個元素就可以訪問了:

image.png

語法細節差異,根據程式碼提示修改即可,完整配置參考末尾示例。

顯示和隱式 buildTypes

在 Kotlin DSL 中,某些 buildTypes(如 debug 和 release,)是隱式提供的。但是,其他 buildTypes 則必須手動建立。

在 Groovy 中,你可能有 debug, release, staging

``` groovy buildTypes { debug {

}
release {

}
staging {

}

} ```

在 KTS 中,僅 debugrelease是隱式提供的,staging必須手動建立:

``` kotlin buildTypes { getByName("debug") {

}
getByName("release") {

}
create("staging") {

}

} ```

參考示例

settings.gradle.kts

kotlin include(":app", ":tdc_core", ":tdc_uicompat", ":tdc_utils")

build.gradle.kts

``` kotlin // Top-level build file where you can add configuration options common to all sub-projects/modules. buildscript { repositories { google() mavenCentral() } dependencies { classpath("com.android.tools.build:gradle:7.0.4") classpath("org.jetbrains.kotlin:kotlin-gradle-plugin:1.7.10")

    // NOTE: Do not place your application dependencies here; they belong
    // in the individual module build.gradle files
}

}

allprojects { repositories { google() mavenCentral() maven("https://jitpack.io/") maven("http://maven.xxx.com/") { // 信任http協議 isAllowInsecureProtocol = true } } configurations.all { resolutionStrategy.apply { cacheChangingModulesFor(0, "seconds") cacheDynamicVersionsFor(0, "seconds") } } }

tasks.register("clean") { delete(rootProject.buildDir) } ```

app/build.gradle.kts

``` kotlin plugins { id("com.android.application") kotlin("android") }

android { compileSdk = 31

defaultConfig {
    applicationId = "com.xx.component"
    minSdk = 21
    targetSdk = 31
    versionCode = 1
    versionName = "1.0"

    multiDexEnabled = true

    testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
}

signingConfigs {
    create("keyStore") {
        keyAlias = "component"
        keyPassword = "123456"
        storeFile = file("xx.keystore")
        storePassword = "123456"
    }
}
buildTypes {
    val signConfig = signingConfigs.getByName("keyStore")
    getByName("debug") {
        isMinifyEnabled = false
        proguardFiles(
            getDefaultProguardFile("proguard-android-optimize.txt"),
            "proguard-rules.pro"
        )
        signingConfig = signConfig
    }
    getByName("release") {
        isMinifyEnabled = false
        proguardFiles(
            getDefaultProguardFile("proguard-android-optimize.txt"),
            "proguard-rules.pro"
        )
        signingConfig = signConfig
    }
    create("staging") {
        isMinifyEnabled = false
        proguardFiles(
            getDefaultProguardFile("proguard-android-optimize.txt"),
            "proguard-rules.pro"
        )
        signingConfig = signConfig
    }
}
compileOptions {
    sourceCompatibility = JavaVersion.VERSION_1_8
    targetCompatibility = JavaVersion.VERSION_1_8
}
kotlinOptions {
    jvmTarget = "1.8"
}
viewBinding.isEnabled = true

}

dependencies { implementation("androidx.core:core-ktx:1.7.0") implementation("androidx.appcompat:appcompat:1.4.1")

testImplementation("junit:junit:4.+")
androidTestImplementation("androidx.test.ext:junit:1.1.3")
androidTestImplementation("androidx.test.espresso:espresso-core:3.4.0")

} ```