用 Kotlin DSL 写 Gradle 脚本

为什么要强调使用 Kotlin 来写 Gradle 脚本,因为这样子可以减少使用者的理解和使用成本,也实在没有必要为了写 Gradle 脚本专门学习一种 DSL(如 Groovy)。本篇文章不会太系统的讲 Gradle 的众知识点,想要系统学习 Gradle 的同学,可以参考大佬整理的笔记 Android Gradle学习笔记。

Gradle Build 生命周期

Gradle 进行构建时,会经历3个生命周期:

  • 初始化阶段
  • 配置阶段
  • 执行阶段
用 Kotlin DSL 写 Gradle 脚本_第1张图片
Gradle Build 生命周期示意图

初始化阶段

初始化阶段确定有多少工程需要构建,创建整个项目层次,并为每个 module 创建一个 Project 对象。(每一个 build.gradle 文件对应一个 project 对象,项目 build.gradle 文件以及众多 module 下的 build.gradle 文件均对应一个 project 对象)。项目初始化阶段会执行 setting.gradle 文件,setting.gradle 中所配置的 module 路径会决定 Gradle 创建哪些 project。

配置阶段

配置阶段会执行 Project 对象和 Task 对象的代码,可以称这个阶段为配置阶段,配置阶段主要执行读取配置参数,创建 Task 对象,根据 Task 之间的依赖关系,构建出有向无环图,进而规定 Task 的执行顺序。对于 task 对象而言,需要明确区分配置和执行这两个阶段。整个配置阶段的运行顺序参照顶层 build.gradle 所代表的 Project 对象 -> setting.gradle 所声明的 Project 对象的顺序执行。

执行阶段

执行阶段会按照配置中规定的顺序执行所有的 Task ,调用 Task 的 doFirst、doLast 方法传入的闭包会存入 Task 的 actions 列表(Task 中的 doFirst、doLast 方法均可调用多次)。

Gradle 生命周期为提供了丰富的回调接口帮助使用者方便的 Hook 整个 Build 流程,可用的函数在上图中均有展示。同时如果你使用的是 Android Studio 或者 IntelliJ 的 Build 窗口中看到配置阶段和执行阶段,在 Build Output 中配置阶段输出以 > Configure project : 开头,执行阶段以 > Task : 开头。

Kotlin DSL 与 Groovy DSL 的差异

Kotlin DSL 在写法上与普通的 Kotlin 程序无差别,不同于 Groovy 灵活的语法,Kotlin 的语法显得更加简洁。主要的区别,同时也是官方迁移指南建议的准备工作:

  • Groovy 字符串可以用单引号 '' 表示,Kotlin 只能用双引号
  • Groovy 对方法的调用可以省略括号 (),Kotlin 不能省略
  • Groovy 赋值时可以省略 =,Kotlin 同样不能省略

除了语法上的区别,别忘了在所有 Groovy 脚本文件的拓展名后加上 .kts 这一后缀。使用 Kotlin DSL 写 Gradle 脚本的体验类似于用 Java 写 Gradle 脚本的体验,要求使用者对 Project、Settings、Gradle 对象的 API 有一定的了解。 接下来按照 gradle.properties 配置 & ext、project-extensions、Task 的顺序来看如何用 Kotlin DSL 实现它们:

project-extensions

这里以 Android 项目中常见的一段配置为例(App Module 下的build.gradle):

Groovy DSL 版本

// 比较1
android {
    compileSdkVersion 30
    buildToolsVersion "30.0.2"

    defaultConfig {
        applicationId "com.example.taskqueue"
        minSdkVersion 16
        targetSdkVersion 30
        versionCode 1
        versionName "1.0"

        testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
    }

    buildTypes {
        // 比较2
        release {
            minifyEnabled false
            proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
        }
    }
    compileOptions {
        sourceCompatibility JavaVersion.VERSION_1_8
        targetCompatibility JavaVersion.VERSION_1_8
    }
    kotlinOptions {
        jvmTarget = '1.8'
    }
}
复制代码

Kotlin DSL 版本

// 比较1
configure {
    compileSdkVersion(30)
    buildToolsVersion("30.0.2")

    defaultConfig {
        applicationId("com.example.taskqueue")
        minSdkVersion(16)
        targetSdkVersion(30)
        versionCode = 1
        versionName = "1.0"

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

    buildTypes {
        // 比较2
        named("release") {
            isMinifyEnabled = false
            proguardFiles(
                getDefaultProguardFile("proguard-android-optimize.txt"),
                "proguard-rules.pro"
            )
        }
    }
    compileOptions {
        sourceCompatibility = JavaVersion.VERSION_1_8
        targetCompatibility = JavaVersion.VERSION_1_8
    }

    kotlinOptions {
        jvmTarget = "1.8"
    }
}
复制代码

可以看出同一份配置 Groovy DSL 版本和 Kotlin DSL 版本的差异不是很明显,排除简单的语法差异(如上面提到的双引号、空格之类的问题),需要注意代码中标出的两个比较处:

比较1: 在 Groovy DSL 中使用的 android {} 其实是一种 Project-Extension,在 app Module 中对应着 BaseAppModuleExtension Extension 类。我们之所以可以在 Groovy DSL 中可以像在 BaseAppModuleExtension 类中给变量赋值是因为 Groovy 提供了一种语法糖,让使用者可以直接用这种写法操作变量。Kotlin DSL 中不能直接这么写,需要使用 configure {} 来访问 BaseAppModuleExtension 中的内容,这里借用 Kotlin 本身的能力同样也能做到类似于 Groovy DSL 的效果。

/**
 * Executes the given configuration block against the [plugin convention]
 * [Convention.getPlugin] or extension of the specified type.
 *
 * @param T the plugin convention type.
 * @param configuration the configuration block.
 * @see [Convention.getPlugin]
 */
inline fun  Project.configure(noinline configuration: T.() -> Unit): Unit =
    typeOf().let { type ->
        convention.findByType(type)?.let(configuration)
            ?: convention.findPlugin()?.let(configuration)
            ?: convention.configure(type, configuration)
    }
复制代码

可以看到 configure 是 Project 对象的拓展方法,实际调用的是 ExtensionContainer 的 findPlugin(Class type) 方法,也就是说 configure 方法实际上等同于:

extensions.findByType().run { 
    // TODO
}
复制代码

相当于 Kotlin DSL 提供的简便方法来配置 Project-Extension 类对象,想要详细了解 Project-Extension 类 ,请参考 Android Gradle学习(五):Extension详解

比较2: Groovy DSL 中配置 buildTypes 可以直接使用 release {} 配置,这是因为 buildTypes 方法实际配置的是一个 NamedDomainObjectContainer 对象,NamedDomainObjectContainer 支持灵活的配置(.e.g 我们可以定义一个默认的名字叫作 release 或者 debug 的对象,也可以自定义一个 full 对象,它们都是 BuildType 类对应的对象),这种灵活的配置同样也靠 Groovy DSL 提供的语法糖提供支持。Kotlin DSL 需要调用 NamedDomainObjectContainer 类里的方法,named 方法就是 NamedDomainObjectContainer 中的方法之一,想要详细了解 NamedDomainObjectContainer 类,请参考 Android Gradle学习(六):NamedDomainObjectContainer详解。

gradle.properties 配置 & ext

如果在 Android 项目中用 Groovy DSL 编写 Gradle 脚本时,通常会用 gradle.properties 和 ext 两种方式定义变量。在 app Module 中举个例子:

app Module 中的 gradle.properties

versionCode = 1
复制代码

project Module 中的 build.gradle

定义 kotlin_version 变量

allprojects {
    ext {
        kotlinVersion = "1.4.10"
    }
}
复制代码

app Moudle 中的 build.gradle

读取 gradle.properties 和 project Module 中的 build.gradle 中定义的变量

println versionCode
println kotlinVersion
复制代码

可以看到 groovy 文件之中可以直接访问 gradle.properties 和 project Module 中的 build.gradle 中定义的变量(gradle.properties 访问范围属于在同一 module 内,项目默认只会创建 project 层级的 gradle.properties 文件)。

Kotlin DSL 定义和访问 gradle.properties 配置 & ext

val versionCode: String by project //1  
val kotlinVersion: String by rootProject.extra  //2
val newKotlinVersion: String by extra("1.3.61")  //3
复制代码

上面这段 Kotlin DSL 代码分别实现了:

  1. 读取 project 的 gradle.properties 文件定义的 versionCode 变量
  2. 读取 project 对应的 build.gradle 文件中定义的 kotlinVersion 变量
  3. 定义一个新的 newKotlinVersion 变量

可以看出在 Kotlin DSL 中访问 gradle.properties 委托给了 Project 对象;定义和去读 Groovy DSL 等价的 ext 变量同样需要委托给 ExtensionAware 接口的 extra 方法

Task

Groovy 中采用 task + 任务名的方式定义一个 Task 的形式在 Kotlin DSL 中同样也行不通,在 Kotlin DSL 中定义 Task 的方法如下:

定义单个 Task

val MyCopy by tasks.create(Copy::class) {
        group = "Copy"
        description = "This is MyCopy Task"
        dependsOn("Copy")
        doFirst {
            println("MyCopy Do First")
        }
        doLast {
            println("MyCopy Do Last")
        }
    }
复制代码

获取单个 Task

val MyCopy by tasks.existing(Copy::class) {
        group = "Copy"
        description = "This is MyCopy Task"
        dependsOn("Copy")
        doFirst {
            println("MyCopy Do First")
        }
        doLast {
            println("MyCopy Do Last")
        }
    }
复制代码

配置多个 Task

tasks {
    val myCopy1 by register(Copy::class) {
        // TODO
    }
    val myCopy2 by existing(Copy::class) {
        // TODO
    }
}
复制代码

Kotlin DSL 创建 Task 建议采用委托属性这样子更简便、直观,如果需要配置多个 Task 建议使用 tasks {} 方法批量创建,由于 tasks 的参数是 TaskContainerScope.() -> Unit 的形式,使用者在 tasks {} 方法中可以直接调用 TaskContainer 的方法而无需引用。

Kotlin DSL 的优缺点

优点

  • 一体性:Kotlin DSL 的优点是未来项目整体向 Kotlin 迁移时,可以保证 Build 脚本和 业务代码采用相同的语言编写,降低了 Build 脚本的理解难度。
  • 与 Groovy 的兼容性:由于 Kotlin 与 Java 之间良好的兼容性保证了 Kotlin 与 Groovy 之间的兼容性。

缺点

  • 缺少错误提示:但是就体验而言并不是那么理想,还有很多需要改进的空间,比如 Groovy DSL 迁移至 Kotlin DSL 的过程中,各种错误缺少提示。
  • 部分写法更复杂:有些写法看起来比 Groovy DSL 更加复杂,有点类似于写 Gradle Plugin 的体验。
  • 有些特性支持的不完全,比如文档中提到的Type-safe model accessors会有引用 plugin 方式的限制,通过这些方式引入的元素无法使用Type-safe model accessors
    • 通过该apply(plugin = "id")方法应用的插件。
    • 项目构建脚本。
    • 脚本插件,通过 apply(from = "script-plugin.gradle.kts")。
    • 通过跨项目配置应用的插件。
    • 满足了以上条件的情况下貌似得经过一次成功的 build 才可以正常调用(这是我的真实体验)。

作者:馒头馒头小馒头
链接:https://juejin.cn/post/6906037038676951053
来源:掘金

你可能感兴趣的:(用 Kotlin DSL 写 Gradle 脚本)