简介

本文假设各位已经有了 Kotlin 基础,对 Kotlin 还不熟悉的小伙伴可以去看我之前发的文章-->​​​​《Kotlin Jetpack 实战》​​​​。

本文将带领各位一步步将 ​​Demo 工程​​ 的 Gradle 脚本改成 Kotlin DSL,让我们一起实战吧!

正文

1. Kotlin 编写 Gradle 脚本的优势

Kotlin

Groovy

自动代码补全

支持

不支持

是否类型安全

不是

源码导航

支持

不支持

重构

自动关联

手动修改

2. 实战前的准备

  • 将 Android Studio 版本升级到最新
  • 将我们的 Demo 工程 clone 到本地,用 Android Studio 打开:​​github.com/chaxiu/Kotl…​​
  • 切换到分支:​​chapter_02_kotlin_dsl_training​
  • 强烈建议读者跟着本文一起实战,实战才是本文的精髓。

3. 开始重构

3-1. 将​​单引号​​​替换成​​双引号​

替换前:

apply plugin: ‘com.android.application

​替换后:​

apply plugin: "com.android.application"

小结:

  • 不用修改 Gradle 文件扩展名,直接使用 Android Studio 替换功能即可。
  • 为什么能够直接替换?因为 Grooovy 和 Kotlin 在字符串定义的语法是相近的:​​双引号​​表示字符串。
  • 那么,为什么要替换呢?因为​​单引号​​​​双引号​​​在 Groovy 里都是定义字符串,而 Kotlin 里​​单引号​​​定义的是​​单个字符​​​,​​双引号​​才是定义字符串。

具体细节可以看我这个 ​​GitHub Commit​​

3-2. 修改 Gradle 文件扩展名

  1. builde.gradle --> build.gradle.​​kts​
  2. settings.gradle --> settings.gradle.​​kts​
  3. Sync 走起!

Script compilation errors: Line 1: include ":app" Unexpected tokens (use ';' > to separate expressions on the same line) Line 1: include ":app" Function invocation 'include(...)' > expected 2 errors

不要慌! 报错不可怕,不报错才可怕!最起码我们知道哪里错了。 错误日志告诉我们,问题出在这里:

// settings.gradle
include ":app"

我们 ​​Command + 鼠标左键​​点击 include,来看看源码实现:

override fun include(vararg projectPaths: String?)

哟!原来 settings.gradle 里面的 include 的本质就是个​​方法调用​​​啊!再结合报错原因:​​Function invocation 'include(...)' > expected​​​,这就单纯是个语法错误呗!就是说,我们改了 Gradle 扩展名以后,IDE 就认为它是个 Kotlin 语句了。而 ​​include ":app"​​用的还是 Groovy 的语法,这当然会报错了!

修改成这样就好了:

// 调用 include 方法,传入一个字符串":app"
include(":app")

接下来重复这个的步骤:Sync --> 报错 --> ​​Command + 鼠标左键​​ 看源码

修改前:

dependencies {
classpath "com.android.tools.build:gradle:4.0.0"
}

​修改后:​

dependencies {
classpath("com.android.tools.build:gradle:4.0.0")
}

3-3. 遇到无法解决的报错怎么办?

比如:如果你继续 Sync,报错的是这里:

task clean(type: Delete) {
delete rootProject.buildDir
}

e: /KotlinJetpackInAction/build.gradle.kts:19:16: Expecting ')' e: ../KotlinJetpackInAction/build.gradle.kts:19:16: Unexpected tokens (use ';' to separate expressions on the same line) e: ../KotlinJetpackInAction/build.gradle.kts:20:23: Expecting an element e: ../KotlinJetpackInAction/build.gradle.kts:20:32: Expecting an element e: ../KotlinJetpackInAction/build.gradle.kts:19:1: Function invocation 'task(...)' expected e: ../KotlinJetpackInAction/build.gradle.kts:19:1: None of the following functions can be called with the arguments supplied: public abstract fun task(p0: String!): Task! defined in org.gradle.api.Project public abstract fun task(p0: String!, p1: Closure<(raw) Any!>!): Task! defined in org.gradle.api.Project public abstract fun task(p0: String!, p1: Action<in Task!>!): Task! defined in org.gradle.api.Project public abstract fun task(p0: (Mutable)Map<String!, *>!, p1: String!): Task! defined in org.gradle.api.Project public abstract fun task(p0: (Mutable)Map<String!, >!, p1: String!, p2: Closure<(raw) Any!>!): Task! defined in org.gradle.api.Project e: ../KotlinJetpackInAction/build.gradle.kts:19:12: Function invocation 'type(...)' expected e: ../KotlinJetpackInAction/build.gradle.kts:19:12: Unresolved reference. None of the following candidates is applicable because of receiver type mismatch: public inline fun ObjectConfigurationAction.type(pluginClass: KClass<>): ObjectConfigurationAction defined in org.gradle.kotlin.dsl e: ../KotlinJetpackInAction/build.gradle.kts:20:5: Function invocation 'delete(...)' expected e: ../KotlinJetpackInAction/build.gradle.kts:20:12: Unresolved reference: rootProject

好可怕,这回一次性报好多错误,而且看起来都很奇怪。怎么办? 不要慌! Kotlin 官方都已经给我们准备好了迁移指南: ​​Migrating build logic from Groovy to Kotlin​​

嫌上面都迁移指南太长?还是纯英文? 不要怕! Kotlin 官方给我们准备了迁移案例: ​​kotlin-dsl-samples:hello-android​​

看!迁移案例里已经告诉我们怎么改这个 clean task 了:

// 具体看这里:https://github.com/gradle/kotlin-dsl-samples/blob/master/samples/hello-android/build.gradle.kts
tasks.register("clean", Delete::class) {
delete(rootProject.buildDir)
}

3-4. 参照​​kotlin-dsl-samples​​继续修改

修改前:

apply plugin: "com.android.application"

​修改后:​

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

修改前:

android {
compileSdkVersion 29

defaultConfig {
applicationId "com.boycoder.kotlinjetpackinaction"
minSdkVersion 21
targetSdkVersion 29
versionCode 1
versionName "1.0"

testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
}

buildTypes {
release {
minifyEnabled false
proguardFiles getDefaultProguardFile("proguard-android-optimize.txt"), "proguard-rules.pro"
}
}
}

​修改后:​

android {
compileSdkVersion(29)

defaultConfig {
applicationId = "com.boycoder.kotlinjetpackinaction"
minSdkVersion(21)
targetSdkVersion(29)
versionCode = 1
versionName = "1.0"

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

buildTypes {
getByName("release") {
isMinifyEnabled = false
proguardFiles(getDefaultProguardFile("proguard-android.txt"), "proguard-rules.pro")
}
}
}

修改前:

dependencies {
//省略部分...
implementation fileTree(dir: "libs", include: ["*.jar"])
implementation "androidx.appcompat:appcompat:1.1.0"
testImplementation "junit:junit:4.12"
androidTestImplementation "androidx.test.ext:junit:1.1.1"
annotationProcessor "com.github.bumptech.glide:compiler:4.8.0"
}

​修改后:​

dependencies {
//省略部分...
implementation(fileTree(mapOf("dir" to "libs", "include" to listOf("*.jar"))))
implementation("androidx.appcompat:appcompat:1.1.0")
testImplementation("junit:junit:4.12")
androidTestImplementation("androidx.test.ext:junit:1.1.1")
annotationProcessor("com.github.bumptech.glide:compiler:4.8.0")
}

具体可以看我这个 ​​Github Commit​​

3-5 大功告成!

这下我们可以开始愉快的用 Kotlin 写 Gradle 脚本了。 那么,这篇文章是不是就该结束了呢?并没有。 本文是以​​实战​​为核心,咱们刚用 Kotlin DSL 重构完项目,当然还要再实战一波啦!

4. Kotlin DSL 实战--依赖管理

4-1. Groovy 时代的依赖管理

以前我们这么定义依赖:

// 根目录下的 builde.gradle
ext {
versions = [
support_lib: "28.0.0",
glide: "4.8.0"
]
libs = [
support_annotations: "com.android.support:support-annotations:${versions.support_lib}",
glide: "com.github.bumptech.glide:glide:${versions.glide}"
]
}

然后这么用:

// app 目录下的 builde.gradle
implementation libs.support_annotations
implementation libs.glide

4-2. Kotlin 时代的依赖管理

Kotlin DSL 管理依赖的方式很多,我这里采用相对主流的做法:​​buildSrc​​​目录下管理。具体细节可以看这个官方文档:​​Gradle Documentation​​

简单翻译:

Gradle 运行的时候,会去检查工程根目录下是否存在​​buildSrc​​目录,如果存在,这个目录下的所有脚本都会自动被添加到工程的环境变量(classpath)里。

借助上面的机制,我们就可以把所有依赖的都以常量形式定义到 ​​buildSrc​​目录下,然后我们就可以直接在工程里随意使用了。

具体结构如下所示:

Kotlin Jetpack 实战 | 02. Kotlin 写 Gradle 脚本是一种什么体验?_ide

ProjectProperties.kt 定义了工程相关的属性:

// ProjectProperties.kt 定义了工程相关的属性
object ProjectProperties {
const val compileSdk = 29
const val minSdk = 21
const val targetSdk = 29

const val applicationId = "com.boycoder.kotlinjetpackinaction"
const val versionCode = 1
const val versionName = "1.0.0"

const val agpVersion = "4.0.0"

Libs.kt 定义了所有的依赖:

object Libs {
const val appCompat = "androidx.appcompat:appcompat:${Versions.appCompat}"
const val constraintlayout = "androidx.constraintlayout:constraintlayout:${Versions.constraintlayout}"

Versions.kt 定义了所有依赖库的版本号:

object Versions {
const val appCompat = "1.1.0"
const val constraintlayout = "1.1.3"

对应 build.gradle.kts 的修改如下:

dependencies {
implementation(fileTree(mapOf("dir" to "libs", "include" to listOf("*.jar"))))
implementation(Libs.appCompat)
implementation(Libs.constraintlayout)
}

具体细节可以看这个 ​​Github Commit​​:

看,现在我们 Gradle 代码就能有自动补全的提示了。

Kotlin Jetpack 实战 | 02. Kotlin 写 Gradle 脚本是一种什么体验?_android_02

真香!

结尾

​注意:​​​在新的工程里用 Kotlin DSL 完全替代 Groovy 是一件很简单的事情,但如果是一个年代久远的工程那就没那么容易了,​​大坑小坑会不少​​,Kotlin DSL 迁移的坑后面会讲。

下一节,我会一步步把 Demo 工程里的 Java 代码重构成 Kotlin。