Gradle迁移指南:从Groovy到KTS

2,853 阅读4分钟

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 中,单引号引起来的也是字符串字面量,因此我们会面对大量这样的写法:

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,所有单引号会变成双引号:

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

给方法调用加上括号

仍以 settings.gradle为例:

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

此处实际是一个方法调用,在 Groovy 中只要没有歧义,就可以把方法调用的括号省略,这在 Kotlin 中是不行的,因此需要统一做加括号处理,采用 全局正则替换 方法:

image.png

  • 匹配框输入正则表达式 (\w+) (([^={\s]+)(.*)),替换框中填写 $1($2),其他配置与前面替换引号一样

检查匹配内容,对匹配错误的部分进行修改,点击 Replace All,所有方法调用都加上了括号:

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"],所以:

ext.kotlin_version = "1.4.30"

等价于

extra["kotlin_version"] = "1.4.30"

接下来就是对 kotlin_version的访问了,需要将它取出来再使用:

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

创建任务

参考 clean任务的修改方式:

// 方法一(推荐使用)
tasks.register<Delete>("clean") {
    delete(rootProject.buildDir)
}

// 方法二
task("clean", Delete::class) {
    delete(rootProject.buildDir)
}

在 Groovy 当中 Delete 类型是作为参数通过 Key-Value 的形式传递的,Kotlin 当中直接把它当做泛型参数传入,这样设计是非常符合 Kotlin 的设计思想。

maven配置

maven语法比较简单,直接修改为:

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

buildTypes {
    debug {
    
    }
    release {
    
    }
    staging {
    
    }
}

在 KTS 中,仅 debugrelease是隐式提供的,staging必须手动创建:

buildTypes {
    getByName("debug") {
    
    }
    getByName("release") {
    
    }
    create("staging") {
    
    }
}

参考示例

settings.gradle.kts

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

build.gradle.kts

// 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<Delete>("clean") {
    delete(rootProject.buildDir)
}

app/build.gradle.kts

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")
}