随着Kotlin语言被Google在Android开发方面的推广,目前较新的Gradle插件也全面拥抱Kotlin,采用Kotlin DSL语言的形式对插件配置进行支持。

Why KTS ?

Kotlin 脚本 (KTS) 比 Groovy 更适合用于编写 Gradle 脚本,因为采用 Kotlin 编写的代码可读性更高,并且 Kotlin 提供了更好的编译时检查和 IDE 支持。Android Gradle 插件 4.0 支持在 Gradle build 配置中使用 KTS。

KTS:是指 Kotlin 脚本,这是 Gradle 在 build 配置文件中使用的一种 Kotlin 语言形式。Kotlin 脚本是可从命令行运行的 Kotlin 代码。

Kotlin DSL:主要是指 Android Gradle 插件 Kotlin DSL,有时也指底层 Gradle Kotlin DSL

在讨论从 Groovy 迁移时,术语“KTS”和“Kotlin DSL”可以互换使用。换句话说,“将 Android 项目从 Groovy 转换为 KTS”与“将 Android 项目从 Groovy 转换为 Kotlin DSL”实际上是一个意思。

我们的项目近期也将构建从Groovy迁移到KotlinDSL,同时我们升级了Android Plugin 、Gradle版本,以及开启了Gradle的Catalog功能管理版本依赖。下面我将对我们项目迁移的过程以及遇到的问题,简单总结一下,这里主要讲述Kotlin DSL配置与Groovy配置的差异

首先声明我们使用的gradle版本和Android Gradle Plugin(AGP)版本分别为:

1
2
3
Gradle Version: 7.2

AGP Version: 7.1.0

具体的AGP和Gradle的版本对照关系可以参考Android Develpers 网站说明:

处理完版本问题我们可以真正的开始改造了:

Step 1 脚本文件命名

.gradle文件重新命名为.gradle.ts文件,例如项目更目录下的settings.gradle重新命名为settings.gradle.kts。

Step 2 转换语法

2.1 为方法调用添加圆括号

Kotlin中方法调用不能省略圆括号,因此需要将原Groovy中省略的圆括号全部添加回来。例如:

1
2
3
4
5
6
7
# Groovy
buildConfigField "String", "BRACH_NAME", "\"${branchName()}\""
implementation 'com.google.android.material:material:1.4.0'

# Kotlin
buildConfigField("String", "BRACH_NAME", "\"${branchName()}\"")
implementation("com.google.android.material:material:1.4.0")

2.2 为属性调用添加 =

Kotlin中所有的属性调用均需要添加赋值等号,如:

1
2
3
4
5
6
7
# Groovy
minSdk 29
targetSdk 30

# Kotlin
minSdk = 29
targetSdk = 30

2.3 字符串转换

Groovy中优先推荐是用单引号设置字符串,Kotlin中则需要使用双引号,因此需要将所有的字符串从单引号改为双引号

2.4 语法/关键字修改

将Groovy语法转换为Kotlin语法,例如:变量声明:Groovy中使用def, Kotlin中使用var或者val

Step 3各个文件具体分析

3.1 settings.gradle.kts与settings.gradle.kts

在settings.gradle.kts中新添加了pluginManagement和dependencyResolutionManagement配置。

  • pluginManagement用来管理插件(包括插件仓储配置,插件寻找策略); -

  • dependencyResolutionManagement用来管理依赖下载的配置。

  • 有了这两个配置之后,原则上build.gradle.kts中可以不用指定buildscript

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
pluginManagement {
includeBuild("build-logic") // 指定编译逻辑Library

//使用的plugin来源仓库
repositories {
google()
mavenCentral()
gradlePluginPortal()
}
// 指定加载策略, 解决部分插件没有 gradle id的问题
resolutionStrategy {
eachPlugin {
if (requested.id.id == "io.objectbox.objectbox-gradle-plugin") {
useModule("io.objectbox:objectbox-gradle-plugin:${requested.version}")
} else if (requested.id.id == "com.google.gms.google.services") {
useModule("com.google.gms:google-services:${requested.version}")
} else if (requested.id.id == "com.google.firebase.firebase-crashlytics-gradle") {
useModule("com.google.firebase:firebase-crashlytics-gradle:${requested.version}")
}
}
}
}
// 依赖资源的来源仓库
dependencyResolutionManagement {
repositories {
google()
mavenCentral()
flatDir {
dirs(
"app/libs",
)
}
}
}

3.2 ${rootProject}/build.gradle.kts 文件

  • ${rootProject}/build.gradle.kts中,在plugins{}中使用id或者alias指定classpath, 此处的apply faslse 代表的是只讲次插件放置到编译过程的classpath中

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    plugins {
    //id需要去gradle插件仓库寻找:https://plugins.gradle.org
    alias(libs.plugins.android.application) apply false
    alias(libs.plugins.kotlin.android) apply false
    alias(libs.plugins.kotlin.jvm) apply false
    id("io.objectbox.objectbox-gradle-plugin") version "2.9.1" apply false
    id("com.google.protobuf") version "0.8.12" apply false
    id("com.google.gms.google.services") version "4.3.10" apply false
    id("com.google.firebase.firebase-crashlytics-gradle") version "2.4.1" apply false
    }

注意:有些老的plugin找不到对应的id,应该怎么处理呢?其实有两种方法:

  • 方法一:直接想之前一样在${rootProject}/build.gradle.kts使用buildscript代码块,声明classpath即可:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    buildscript {
    repositories {
    google()
    jcenter()
    mavenCentral()
    }
    dependencies {
    classpath "com.google.protobuf:protobuf-gradle-plugin:$protobuf_gradle_plugin_version"
    classpath "com.google.gms:google-services:${googleServiceVersion}"
    classpath "com.google.firebase:firebase-crashlytics-gradle:${crashlyticGradleVersion}"
    }
    }
  • 方法二: 给插件自定义一个id名称,然后在pluginManagement中处理寻找策略:

    1. ${rootProject}/build.gradle.kts文件
    1
    2
    3
    4
    5
    6
    7
    plugins {
    //使用自定义的id声明
    id("io.objectbox.objectbox-gradle-plugin") version "2.9.1" apply false
    id("com.google.protobuf") version "0.8.12" apply false
    id("com.google.gms.google.services") version "4.3.10" apply false
    id("com.google.firebase.firebase-crashlytics-gradle") version "2.4.1" apply false
    }

. ${rootProject}/settings.gradle.kts中指定id寻找策略

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
pluginManagement {
...
resolutionStrategy {
// 自定义插件id的寻找策略
// 解决部分插件没有 gradle id的问题
eachPlugin {
if (requested.id.id == "io.objectbox.objectbox-gradle-plugin") {
useModule("io.objectbox:objectbox-gradle-plugin:${requested.version}")
} else if (requested.id.id == "com.google.gms.google.services") {
useModule("com.google.gms:google-services:${requested.version}")
} else if (requested.id.id == "com.google.firebase.firebase-crashlytics-gradle") {
useModule("com.google.firebase:firebase-crashlytics-gradle:${requested.version}")
}
}
}
}
  • 关于gradle是如何通过id寻找插件的?

    关于这个话题可以单独展开讲,此处简单说一下,gradle通过id查找plugin的规则可以简单理解为通过将id绑定到一个pom文件,然后pom文件里指定plugin的module资源:

    根据id从仓库找寻pom依赖文件规则:https://plugins.gradle.org/m2/{id拼接的path}/{id}.gradle.plugin/{version}/{id}.gradle.plugin-{version}.pom

    例如寻找com.google.protobuf插件:https://plugins.gradle.org/m2/com/google/protobuf/com.google.protobuf.gradle.plugin/0.7.3/com.google.protobuf.gradle.plugin-0.7.3.pom

    请求到的pom内容为

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    <project xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns="http://maven.apache.org/POM/4.0.0" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
        <modelVersion>4.0.0</modelVersion>
        <groupId>com.google.protobuf</groupId>
        <artifactId>com.google.protobuf.gradle.plugin</artifactId>
        <version>0.7.3</version>
        <packaging>pom</packaging>
        <dependencies>
        <dependency>
            <groupId>gradle.plugin.com.google.protobuf</groupId>
            <artifactId>protobuf-gradle-plugin</artifactId>
            <version>0.7.3</version>
        </dependency>
        </dependencies>
    </project>

3.3 app/build.gradle.kts文件

  • 注意点一:本地jar引入

    1
    2
    3
    4
    5
    6
    7
    implementation(fileTree("libs") {
    include("*.jar")
    })

    api(fileTree("libs") {
    include("*.jar")
    })
  • 注意点二: 本地aar引入

    引入本地aar文件特别需要注意,必须使用指定方式引入

    1
    2
    3
    implementation(linkedMapOf("name" to "mocap4face-0.4.0", "ext" to "aar"))

    api(linkedMapOf("name" to "mocap4face-0.4.0", "ext" to "aar"))

注意:本地aar不能使用以下方式引入, 否则会出现debug可以编译,release包编译报错。具体报错为:

What went wrong:
Execution failed for task ‘:news_memorycanary_release:bundleReleaseLocalLintAar’.

Error while evaluating property ‘hasLocalAarDeps’ of task ‘:news_memorycanary_release:bundleReleaseLocalLintAar’
Direct local .aar file dependencies are not supported when building an AAR. The resulting AAR would be broken because the classes and Android resources from any local .aar file dependencies would not be packaged in the resulting AAR. Previous versions of the Android Gradle Plugin produce broken AARs in this case too (despite not throwing this error). The following direct local .aar file dependencies of the :news_memorycanary_release project caused this error: /Users/themachine/.jenkins/workspace/android-news-client-feature-2023-sprint-adaptation_t_u_new/news_memorycanary_release/libs/memorycanary_core_1.1.1.aar

1
2
3
4
5
6
//错误的aar引入方式一
implementation(fileTree("libs") {
include("*.aar")
})
//错误的aar引入方式二
implementation(file("libs/demo.aar"))

3.4 ext扩展使用

使用ext的get和set方法,注意使用时需要指定声明ext的project,例如

  • ${rootProject}/build.gradle.kts中声明

    1
    2
    3
    ext {
    set("androidCompileSdkVersion", 31)
    }
  • 使用androidCompileSdkVersion时,需要指定rootProject

    app/build.gradle.kts中使用

    1
    2
    3
    android {
    println(rootProject.ext.get("androidCompileSdkVersion"))
    }

到此为止,基本上从groovy迁移到kts,所遇到的主要问题就说完了。其他相关的问题,例如catalog的使用,includeBuild引入编译逻辑等,后续单独文章详细记录。