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

7,942 阅读5分钟

简介

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

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

正文

1. Kotlin 编写 Gradle 脚本的优势

KotlinGroovy
自动代码补全支持不支持
是否类型安全不是
源码导航支持不支持
重构自动关联手动修改

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
    1. settings.gradle --> settings.gradle.kts
    1. 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?) =
    delegate.include(*projectPaths)

哟!原来 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目录下,然后我们就可以直接在工程里随意使用了。

具体结构如下所示:

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 DSL 完全替代 Groovy 是一件很简单的事情,但如果是一个年代久远的工程那就没那么容易了,大坑小坑会不少,Kotlin DSL 迁移的坑后面会讲。

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

都看到这了,点个赞呗!

下一章-->《Kotlin 编程的三重境界》

目录-->《Kotlin Jetpack 实战》