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的 全局正则替换
:
- 匹配框输入正则表达式
'(.*?[^\])'
,替换框中填写"$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 中是不行的,因此需要统一做加括号处理,采用 全局正则替换
方法:
- 匹配框输入正则表达式
(\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创建索引完毕,各个元素就可以访问了:
语法细节差异,根据代码提示修改即可,完整配置参考末尾示例。
显示和隐式 buildTypes
在 Kotlin DSL 中,某些 buildTypes
(如 debug
和 release,
)是隐式提供的。但是,其他 buildTypes
则必须手动创建。
在 Groovy 中,你可能有 debug
, release
, staging
:
buildTypes {
debug {
}
release {
}
staging {
}
}
在 KTS 中,仅 debug
和release
是隐式提供的,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")
}