How to migrate Kotlin DSL

Photo by Larry Li on Unsplash

相信各位Android工程師對Groovy應該相對的不陌生,有接觸過卻又不是全然非常了解,但它相對於專案卻也佔了舉足輕重的角色。轉換成Kotlin DSL網路上應該也有一些相對應的資源了,但相對於Groovy來說還是少了些,所以就將我的經驗分享上來希望對看到這篇文章的你有那麼一點點的幫助。

Kotlin DSL

Kotlin在1.3.20 Released版本宣布了他們新的Multiplatform Plugin支援Kotlin Gradle DSL,所以我們就能進一步改善專案的Build scripts,管理起來也相對輕鬆一點。

在管理Gradle Dependencies有下列三種方式:

  1. 手動管理
    手動管理就是最基本的Dependencies一直添加的方式,有版本更新的話Android Studio也會亮綠底提醒你,麻煩之處在於你要一個一個維護,量一大時是一件很費時的工作。

例子如下:

implementation "com.android.support:support-annotations:27.0.2"
implementation "com.android.support:appcompat-v7:27.0.2"
implementation "com.squareup.retrofit2:retrofit:2.3.0"
implementation "com.squareup.retrofit2:adapter-rxjava2:2.3.0"
  1. Google推薦的ext
    這是Google推薦的方法,使用Gradle Extra Properties,好處是升級同一版號的Library只要更改一處的版號就行了。

Dependency.gradle例子如下:

ext {
versions = [
support_lib: "27.0.2",
retrofit: "2.3.0"
]
libs = [
support_annotations: "com.android.support:support-annotations:${versions.support_lib}",
support_appcompat_v7: "com.android.support:appcompat-v7:${versions.support_lib}",
retrofit :"com.squareup.retrofit2:retrofit:${versions.retrofit}",
retrofit_rxjava_adapter: "com.squareup.retrofit2:adapter-rxjava2:${versions.retrofit}"
]
}

你要implement的地方:

implementation libs.support_annotations
implementation libs.support_appcompat_v7
implementation libs.retrofit
implementation libs.retrofit_rxjava_adapter
  1. Kotlin + buildSrc
    接下來的範例會使用這個方法來說明,所以可以參考下面的步驟來實作,但實做起來概念與Google推薦的ext有幾分相似的味道。

Step 1.

首先將你的專案資料夾顯示切成“Project”並且在專案的根目錄新增一個buildSrc的module。

Cowman

創立BuildSrc module

接著像上面德資料層級一樣創立些檔案,那創立這個buildSrc的目的是為了manage dependencies和取得IDE completion的支援。

Step 2.

之後在build.gradle.kts加上這些code。

repositories {
jcenter()
}
plugins {
`kotlin-dsl`
}

Step 3.

再來在Version的檔案加入你自定義的版本號。

object Versions {
val support_lib = "27.0.2"
val retrofit = "2.3.0"
}

Step 4.

接著在Dependencies加入下面的Library。

object Dependencies {
val support_annotations = "com.android.support:support-annotations:${Versions.support_lib}"
val support_appcompat_v7 = "com.android.support:appcompat-v7:${Versions.support_lib}"
val retrofit = "com.squareup.retrofit2:retrofit:${Versions.retrofit}"
val retrofit_rxjava_adapter = "com.squareup.retrofit2:adapter-rxjava2:${Versions.retrofit}"
}

Step 5.

Dependencies跟Version定義好後就能在你implementation的地方使用它了,到目前為止就是Kotlin + buildSrc的步驟

implementation Dependencies.support_annotations
implementation Dependencies.support_appcompat_v7
implementation Dependencies.retrofit
implementation Dependencies.retrofit_rxjava_adapter

基本原則

完成上面的Kotlin + buildSrc後接下來只要注意一些大原則就能快樂升級了。那Groovy跟Kotlin DSL是可以混用的,不是指你寫到一半可以換寫Groovy接著寫Kotlin DSL喔!是指一整個專案,跟你要將專案更改成Kotlin時一樣,可以先替換一些簡單的檔案,一樣是可以建置App的。

那接下來我們來看看基本原則有哪些,裡面也會穿插些你可能會用到的東西。

副檔名

有搜過資料的話應該都知道副檔名是不一樣的,原本是"xxx.gradle"更換成Kotlin DSL後副檔名是換成"xxx.gradle.kts"

字串符號

在Groovy大家看到的字串符號一般都以'xxx'作為表示,在Kotlin DSL則是以雙引號標示,所以將'xxx'替換成"xxx"吧!

DefaultConfig

  • Groovy
defaultConfig {
applicationId "com.example.myapplication"
minSdkVersion 21
targetSdkVersion 28
versionCode 1
versionName "1.0"
}
  • Kotlin DSL
defaultConfig {
applicationId = "com.example.myapplication"
minSdkVersion("21")
targetSdkVersion("28")
versionCode = 1
versionName = "1.0"
}

Task

在Task上可以看到樣子長得不太一樣了,那官方網站也有提到在創建一個任務時可以使用register或是create進行創建,如果想要了解可以參考官網Creating tasks

  • Groovy
task ktlintFormat(type: JavaExec, group: "formatting") {
description = "Fix Kotlin code style deviations."
main = "com.github.shyiko.ktlint.Main"
classpath = configurations.ktlint
args "-F", "src/**/*.kt"
}
  • Kotlin
tasks.register<JavaExec>("ktlintFormat") {
group = "formatting"
description = "Fix Kotlin code style deviations."
classpath = ktlint
main = "com.pinterest.ktlint.Main"
args("--android", "-F", "src/**/*.kt")
}

Plugin

在看別人的文章有提到id是引用plugin的標準方式,如果引用是kotlin的plugin的話可以省略成使用kotlin帶入,那他也只是省略了org.jetbrains.kotlin,封裝了一下id而已。

  • Groovy
apply plugin: 'com.android.application'
apply plugin: 'kotlin-android'
apply plugin: 'kotlin-android-extensions'
apply plugin: 'kotlin-kapt'
  • Kotlin
plugins {
id("com.android.application")
kotlin("android")
kotlin("android.extensions")
kotlin("kapt")
}

Apply

再來就是會有引用其他檔案的問題,可以使用apply的方式來加入,除了檔案外假設你不想像上面一樣寫成一個區塊加入plugins也可以使用apply的方式來加入。

  • Groovy
apply from: 'custom.gradle'
apply plugin: 'jacoco'
  • Kotlin
apply(from = "custom.gradle")
apply(plugin = "jacoco")

Implementation

我在implementation上是保持Groovy的狀態因為groovy才有dependencyGroup但在kotlin dsl上我沒有找到相對應的方法;那在瀏覽資料時看到有網友說在implementation aar時有遇到bug可以看Github issue

  • Groovy
implementation fileTree(include: '*.jar', dir: 'libs')
implementation(name: 'xxx', ext: 'aar')
  • Kotlin
implementation(fileTree(mapOf("include" to listOf("*.jar"), "dir" to "libs")))
implementation (group="",name="xxx",ext = "aar")

Build variants

接下來這也是在專案中很常使用的,怎會依照開發或者正式版下去建置相對應的配置,那可以參考下面的code來建立你的build variants,完成後我會接著說明你內心中滿滿的疑問,因為我這裡也研究了一段時間。

  • Groovy
signingConfigs {
debug {
storeFile file(keyStorePath)
keyAlias 'myapplication'
storePassword KEYSTORE_PASSWORD
keyPassword KEY_PASSWORD
}
release {
storeFile file(keyStorePath)
keyAlias 'myapplication'
storePassword KEYSTORE_PASSWORD
keyPassword KEY_PASSWORD
}
}
productFlavors {
dev {
resValue "string", "application_name", "(DEV)myapplication"
applicationIdSuffix ".dev"
}
uat {
resValue "string", "application_name", "(UAT)myapplication"
applicationIdSuffix ".uat"
}
}
buildTypes {
debug {
minifyEnabled false
}
release {
debuggable false
minifyEnabled true
proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
}
}
  • Kotlin
signingConfigs {
named("debug").configure {
storeFile = file("../myapplication.keystore")
keyAlias = "myapplication"
this.storePassword = storePassword
this.keyPassword = keyPassword
}
register("release") {
storeFile = file("../myapplication.keystore")
keyAlias = "myapplication"
this.storePassword = storePassword
this.keyPassword = keyPassword
}
}
productFlavors {
create("dev") {
resValue("string", "application_name", "(DEV)myapplication")
applicationIdSuffix = ".dev"
}
create("uat") {
resValue("string", "application_name", "(UAT)myapplication")
applicationIdSuffix = ".uat"
}
}
buildTypes {
getByName("debug") {
isMinifyEnabled = false
}
getByName("release") {
isDebuggable = false
isMinifyEnabled = true
proguardFiles(getDefaultProguardFile("proguard-android.txt"), "proguard-rules.pro")
}
}

這裡可能會有疑問的會是register、create跟getByName,還有signingConfigs裡的設定,為什麼不能都用named還是register呢?

首先signingConfigs裡的設定方式為什麼不一樣,首先我們進去signingConfigs看後可以看到參數型態是Action<NamedDomainObjectContainer>,那我們的線索是NamedDomainObjectContainer。

從Api文件NamedDomainObjectContainer裡面看到這些方法,了解了register、create跟getByName有什麼不同後。

再來看看Android Gradle open source,裡面BuildType裡面都有create release跟debug,但是在signingConfigs裡卻只有debug被create,所以才會造就signingConfigs裡release跟debug的不同。

那你可能也會看到另一種使用create的版本,這也沒什麼不好只是看到API文件中的這段話通常,使用此方法代替NamedDomainObjectContainer.create(java.lang.String)效率更高,因为该方法将急切地创建对象,而不管当前构建是否需要该对象. 另一方面,此方法将推迟创建,直到需要为止.,我就改用register去實作它了。

同場加映取得Git commit hash、branch name

  • Groovy
//取得當前的Git commit hash
def getGitHash = { ->
def stdout = new ByteArrayOutputStream()
exec {
commandLine 'git', 'rev-parse', '--short', 'HEAD'
standardOutput = stdout
}
return stdout.toString().trim()
}
//取得當前的Git branch name
def gitBranch = { ->
def branch = ""
def HEAD = "git rev-parse --abbrev-ref HEAD".execute()
HEAD.in.eachLine { line -> branch = line }
HEAD.err.eachLine { line -> println line }
HEAD.waitFor()
return branch
}
  • Kotlin
fun runCommand(project: Project, command: String): String {
val stdout = ByteArrayOutputStream()
project.exec {
commandLine = command.split(" ")
standardOutput = stdout
}
return stdout.toString().trim()
}
//取得當前的Git commit hash
val gitLastCommitHash = runCommand(project, "git rev-parse --short HEAD")
//取得當前的Git branch name
val gitBranchName = runCommand(project, "git rev-parse --abbrev-ref HEAD")

同場加映取得當下時間

Kotlin的比較麻煩一點在buildSrc裡create一個object file才能做到拿時間這件事情。

  • Groovy
def formattedDate = new Date().format('yyyyMMddHHmmss')
  • Kotlin
object Utils {
fun getFormattedDate(): String {
return SimpleDateFormat("yyyyMMddHHmmss").format(Date()).orEmpty()
}
}

總結

本來想說先大略寫轉成Kotlin DSL的過程,後來發現這基礎知識也是很多,如果大略寫一下又覺得很可惜,所以後來就完整寫出轉成Kotlin DSL的過程發現篇幅有點長,如果再加個UI test screenshot的話這篇應該會爆炸長,所以後來就先改變下方向只單純講述轉成Kotlin DSL的過程,基本上遇到的障礙是沒有太多,用心Google下是有的。

那也有人說Kotlin DSL的編譯速度慢很多,基本上我看到那篇issue是兩年前了,後續也沒看到有人回報相關的issue所以可能已經修復了,目前使用上也沒有太大問題。如果有更好的方法也歡迎留言跟我說!

這篇就先當作UI test screenshot的前導吧!希望這篇對現在看文章的你有幫助,下次見拉~

參考

--

--

陳建維 Ben
工程師求生指南(Sofware Engineer Survival Guide)

喜愛新鮮知識充滿好奇心的Mobile工程師,3C愛好者也是書蟲。連絡信箱:tttw216@gmail.com;目前遷移至我的Blog: https://awilab.com/