Android 构建脚本从 Groovy 迁移到 Kotlin DSL
网上已经有不少关于 Kotlin-DSL 很好的文章,但并未能完全解答我迁移过程中遇到的疑问。通过查阅各类文档和源码,我找到了想要的答案。这篇文章记录了我的迁移过程以及对各类问题追溯到源头的答案。
为什么要迁移?
因为 Groovy 是动态语言,在用作 Android 构建脚本的时候,经常有些问题:
- 很差的IDE支持(自动提示等)
- 性能问题
- 很多错误在
build时才报出,而不是编译期 - 难以debug
- 重构很麻烦
…
kotlin 并非动态语言,但却兼具了 Groovy 的灵活性和静态语言的特点,是一种类型安全的 DSL,很大程度上解决了上述的问题。
所以,开始吧!!
环境准备:
- 升级 Gradle Wrapper 到5.0+
1
2
3
4
5
6# gradle-wrapper.properties
distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-5.1-all.zip - Gradle 5.0 只支持Java 8及以上,确保你的Java环境满足要求
- 升级所有的Gradle 插件
基本原则
由于Groovy和kotlin语言特性上的不同,我们迁移时需要遵循以下基本原则。
字符串
Groovy 中字符串单双引号都行,但是kotlin中字符串必须是双引号。所以第一步先把所有的build.gradle和settings.gradle里的''全部替换为""
赋值/传值
在Groovy中xx yy可以表示对变量xx赋与值yy,也可以表示调用函数xx,yy是参数,如Android中常见的:
1 | defaultConfig { |
而在kotlin中则是和Java一样的语法,相应的,上面的栗子迁移后:
1 | defaultConfig { |
知道了这个区别,但是很多
xx yy到底是赋值还是函数调用不好分辨,比如versionName "1.0"是赋值,而minSdkVersion 16居然是函数调用。我们可以利用Android Studio的辅助功能,在kotlin-dsl 中是可以快速点选查看方法的源码的(mac 下cmd + click),而对属性变量就没啥反应,所以可以根据这个小trick来分辨。
Task
由于Koltin 是静态类型语言,Groovy是动态语言,前者是类型安全的,他们的性质区别很明显的体现在了 task 的创建和配置上。详情可以参考Gradle官方迁移教程
1 | // groovy |
迁移步骤
- 字符串单引号变双引号
- 重命名所有
xx.gradle脚本文件,加上.kts后缀 - 插件声明 DSL的目标是尽可能具有声明性。 Kotlin DSL 使用
plugins代码块来生成静态扩展函数以利用这些插件。因此,我们需要将之前Groovy中所有apply plugin xxx移动到一个plugins代码块中:1
2
3
4
5// Groovy
apply plugin: 'com.android.application'
apply plugin: 'kotlin-android'
apply plugin: 'kotlin-android-extensions'
apply plugin: 'kotlin-kapt'1
2
3
4
5
6
7// Kotlin-dsl
plugins {
id("com.android.application")
kotlin("android")
kotlin("android.extensions")
kotlin("kapt")
}id()是应用插件的标准方法,kotlin()是应用Kotlin插件的方法,我们查看源码可以发现,它只是封装了一下id()方法,省略了org.jetbrains.kotlin.:1
2fun PluginDependenciesSpec.kotlin(module: String): PluginDependencySpec =
id("org.jetbrains.kotlin.$module") - 按前面提到的基本原则改写所有语句
示例
apply
除了apple 插件外,apply 还可以用来应用其他的gradle文件:
1 | //groovy |
依赖
依赖管理作为 gradle 的基本功能,gradle 提供了非常丰富的方法,下面以分三种情形进行说明。
基本情形
1 | // groovy |
fileTree
使用 groovy 添加本地目录 jar 包依赖:
1 | // groovy |
查看 fileTree 函数源码:
1 | //org.gradle.api.Project |
需要传入的是Map,因此迁移到 kotlin-dsl 可以这样写:
1 | implementation(fileTree(mapOf("include" to listOf("*.jar"), "dir" to "libs"))) |
特别类型库依赖
之前在groovy中添加特别类型库依赖:
1 | implementation(name: 'splibrary', ext: 'aar') |
按前面提到的基本原则,我们会这样改写:
1 | // wrong |
但实际上会导致失败。跳转到 kotlin-dsl implementation的源码找原因:
1 | fun DependencyHandler.`implementation`(dependencyNotation: Any)... |
可以看到implementation有许多重载方法,可以用来处理各种依赖来源及设置的条件。在第三种实现对应的是我们的情况,其中group参数是必填的。
因此在kotlin-dsl中需要这样写:
1 | implementation (group="",name="splibrary",ext = "aar") |
必须加一个group还是很奇怪的,而目前看来这是一个bug (Gitub issue)
build variants
构建变体代表我们可以为应用构建的一个不同版本,比如“全功能release版”,”全功能”就是一种 productFlavor, “release” 则是一种 buildType,通过自定义并组合多种 productFlavor 和 buildType 可以构建多种不同版本的应用。
在 Groovy 中:
1 | productFlavors { |
在 kotlin-dsl 中:
1 | productFlavors { |
说明:
create()和getByName()是哪儿来的?demo,full,release等为什么作为字符串变成函数的参数了?在Groovy中,
buildTypes和productFlavors以及signingConfigs这几个函数都接收一个Action<? super NamedDomainObjectContainer<T>>的接口作为参数,Action是一个函数式接口,其唯一的方法接收一个NamedDomainObjectContainer接口的实现类对象作为参数,而这个接口的主要方法就包含了create()和getByName(),这俩方法有一些重载方法,但都包含一个字符串参数作为T的对象标识name。在 Groovy 中的full,release等值都会作为这个参数的值传入。以
signingConfigs为例,用 Java 代码来演示可以更详细的展示这个调用层级:1
2
3
4
5
6
7signingConfigs(signingConfigContainer -> {
signingConfigContainer.create("release",signingConfig ->{
signingConfig.setStorePassword("xxxx");
signingConfig.setKeyAlias("xx.yy.zz");
});
});*
signingConfigContainer就是接口NamedDomainObjectContainer<SigningConfig>的实现类。create()和getByName()在什么时候使用?在
NamedDomainObjectContainer和其子接口NamedDomainObjectCollection的Api 文档中已详细说明,简而言之,当其中的元素(i.e.productFlavor/buildType)已经存在则调用getByName(),否则使用create()创建新对象。而
buildTypes中的debug和release以及 signingConfigs 中的debug都是默认存在的,通过查看Android Gradle插件的源码可以发现这一点:1
2
3
4
5// com.android.build.gradle.AppPlugin
signingConfigContainer.create(DEBUG)
buildTypeContainer.create(DEBUG)
buildTypeContainer.create(RELEASE)因此这三者是调用
getByName()方法进行配置的。在属性配置的代码块中,boolean 类型的赋值为什么有的在原来的表示(指 Groovy 中的表示)前加了is (e.g. minifyEnabled),有的不加 (e.g. multiDexEnabled)?
这其实就是 kotlin 和 java 的互调用规则决定的。 我们进行属性配置的这些类,即 SigningConfig BuildType 等,其源码都是 Java。
从 kotlin 中调用 Java,遵循 getterter 和 setter 的 Java 约定的方法(名称以
get开头的无参数方法和名称以set开头的单参数方法)在 Kotlin 中表示为属性,对boolean类型属性有两种情况:- 当 Java 类中的某boolean类型属性的
getter/setter方法形如setXxx()isXxx()的时候,kotlin进行取值或赋值调用都是isXxx:
1
2
3if(!isXxx){
isXxx = true
}- 形如通常的
getXxx()和setXxx()的时候,kotlin的调用则不带前缀is。
跳转到BuildType等的源码可以发现,minifyEnabled属于第一种情况,而multiDexEnabled属于第二种情况,因此转为kotlin时前者需要带前缀is。
- 当 Java 类中的某boolean类型属性的
访问gradle.properties中的配置
我们通常会把签名信息、版本信息等配置写在gradle.properties中,groovy访问的时候直接引用即可:
1 | # gradle.properties |
而在kotlin-dsl的早期版本 (v0.16.3, Gradle 4.7 之前) 却无法直接访问,现在可以通过委托属性来访问:
1 | # build.gradle.kts |
关于 ext
Google 官方推荐的一个 Gradle 配置最佳实践是在项目最外层 build.gradle 文件的ext代码块中定义项目范围的属性,然后在所有模块间共享这些属性,比如我们通常会这样存放依赖的版本号。
1 | // build.gradle |
但是由于缺乏IDE的辅助(跳转查看、全局重构等都不支持),实际使用体验欠佳。
随着 Gradle 5.0的发布,Gradle官方现在推荐将这些内容声明在一个专用文件夹buildSrc中。App目录结构如下:
1 | . |
依赖和版本号等值存放在了Depends.kt和Versions.kt文件中。
具体操作可以参考 Sam Edwards 的视频教程,以及 DroidKaigi 的2018开发者大会的 App 源码
尾巴
如文章开头所述,Kotiln-dsl 是一种更友好的 DSL语言,它是类型安全的,具有完善的IDE辅助支持(代码自动提示补全,快速跳转等),语法也非常灵活,同时也有很好的可读性,虽然目前与Groovy的兼容还没有100%完成,但对功能基本没有什么影响。
但目前的kotlin-dsl也有一些问题,如 ProAndroidDev 的文章中提到的,因为额外的类型安全检查,kotlin-dsl在性能上会有所损失,大型项目在迁移之前做个性能测试会更稳妥。
References
- Android Gradle Kotlin DSL 迁移
- Apache Groovy - 字符串表示
- ProAndroidDev - Migrating Android build scripts from Groovy to Kotlin DSL
- Gradle Guides - Migrating build logic from Groovy to Kotlin
- Gradle Docs - Organizing Gradle Projects
- kotlin - Type-Safe Builders
- kotlin-dsl Github issues - accessing gradle properties
- kotlin-dsl samples