将构建配置从 Groovy 迁移到 KTS

将构建配置从 Groovy 迁移到 KTS

icon.jpg

前言

作为Android开发习惯了面向对象编程,习惯了IDEA提供的各种辅助开发快捷功能。

那么带有陌生的常规语法的Groovy脚本对于我来说一向敬而远之。

Kotlin DSL的出现感觉是为了我们量身定做的,因为采用 Kotlin 编写的代码可读性更高,并且 Kotlin 提供了更好的编译时检查和 IDE 支持。

名词概念解释

  • Gradle: 自动化构建工具. 平行产品: Maven.

  • Groovy: 语言, 编译后变为JVM byte code, 兼容Java平台.

  • DSL: Domain Specific Language, 领域特定语言.

  • Groovy DSL: Gradle的API是Java的,Groovy DSL是在其之上的脚本语言. Groovy DS脚本文件后缀: .gradle.

  • KTS:是指 Kotlin 脚本,这是 Gradle 在构建配置文件中使用的一种 Kotlin 语言形式。Kotlin 脚本是可从命令行运行的 Kotlin 代码。

  • Kotlin DSL:主要是指 Android Gradle 插件 Kotlin DSL,有时也指底层 Gradle Kotlin DSL

在讨论从 Groovy 迁移时,术语“KTS”和“Kotlin DSL”可以互换使用。换句话说,“将 Android 项目从 Groovy 转换为 KTS”与“将 Android 项目从 Groovy 转换为 Kotlin DSL”实际上是一个意思。

Groovy和KTS对比

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

优点:

  • 可以使用Kotlin, 开发者可能对这个语言更熟悉更喜欢.
  • IDE支持更好, 自动补全提示, 重构,imports等.
  • 类型安全: Kotlin是静态类型.
  • 不用一次性迁移完: 两种语言的脚本可以共存, 也可以互相调用.

缺点和已知问题:

  • 目前,采用 KTS 的构建速度可能比采用 Groovy 慢(自测小demo耗时增加约40%(约8s))。

  • Project Structure 编辑器不会展开在 buildSrc 文件夹中定义的用于库名称或版本的常量。

  • KTS 文件目前在项目视图中不提供文本提示

Android构建配置从Groovy迁移KTS

准备工作

  1. Groovy 字符串可以用单引号 'string' 或双引号 "string" 引用,而 Kotlin 需要双引号 "string"

  2. Groovy 允许在调用函数时省略括号,而 Kotlin 总是需要括号。

  3. Gradle Groovy DSL 允许在分配属性时省略 = 赋值运算符,而 Kotlin 始终需要赋值运算符。

所以在KTS中需要统一做到:

  • 使用双引号统一引号.
groovy-kts-diff1.png
  • 消除函数调用和属性赋值的歧义(分别使用括号和赋值运算符)。
groovy-kts-diff2.png

脚本文件名

Groovy DSL 脚本文件使用 .gradle 文件扩展名。

Kotlin DSL 脚本文件使用 .gradle.kts 文件扩展名。

一次迁移一个文件

由于您可以在项目中结合使用 Groovy build 文件和 KTS build 文件,因此将项目转换为 KTS 的一个简单方法是先选择一个简单的 build 文件(例如 settings.gradle),将其重命名为 settings.gradle.kts,然后将其内容转换为 KTS。之后,确保您的项目在迁移每个 build 文件之后仍然可以编译。

自定义Task

由于Koltin 是静态类型语言,Groovy是动态语言,前者是类型安全的,他们的性质区别很明显的体现在了 task 的创建和配置上。详情可以参考Gradle官方迁移教程

// groovy
task clean(type: Delete) {
    delete rootProject.buildDir
}
// kotiln-dsl
tasks.register("clean", Delete::class) {
    delete(rootProject.buildDir)
}
val clean by tasks.creating(Delete::class) {
    delete(rootProject.buildDir)
}
open class GreetingTask : DefaultTask() {
    var msg: String? = null
    @TaskAction
    fun greet() {
        println("GreetingTask:$msg")
    }
}
val msg by tasks.creating(GreetingTask::class) {}
val testTask: Task by tasks.creating {
   doLast {
       println("testTask:Run")
   }
}
val testTask2: Task = task("test2") {
    doLast { 
      println("Hello, World!") 
    }
}
val testTask3: Task = tasks.create("test3") {
    doLast {
        println("testTask:Run")
    }
}

使用 plugins 代码块

如果您在build 文件中使用 plugins 代码块,IDE 将能够获知相关上下文信息,即使在构建失败时也是如此。IDE 可使用这些信息执行代码补全并提供其他实用建议,从而帮助您解决 KTS 文件中存在的问题。

在您的代码中,将命令式 apply plugin 替换为声明式 plugins 代码块。Groovy 中的以下代码…

apply plugin: 'com.android.application'
apply plugin: 'kotlin-android'
apply plugin: 'kotlin-kapt'
apply plugin: 'androidx.navigation.safeargs.kotlin'

在 KTS 中变为以下代码:

plugins {
    id("com.android.application")
    id("kotlin-android")
    id("kotlin-kapt")
    id("androidx.navigation.safeargs.kotlin")
 }

如需详细了解 plugins 代码块,请参阅 Gradle 的迁移指南

注意plugins 代码块仅解析 Gradle 插件门户中提供的插件或使用 pluginManagement 代码块指定的自定义存储库中提供的插件。如果插件来自插件门户中不存在的 buildScript 依赖项,那么这些插件在 Kotlin 中就必须使用 apply 才能应用。例如:

apply(plugin = "kotlin-android")
apply {
    from("${rootDir.path}/config.gradle")
    from("${rootDir.path}/version.gradle.kts")
}

如需了解详情,请参阅 Gradle 文档

强烈建议您plugins {}优先使用块而不是apply()函数。

有两个关键的最佳实践可以更轻松地在 Kotlin DSL 的静态上下文中工作:

  • 使用plugins {}
  • 将本地构建逻辑放在构建的buildSrc目录中

plugins {}块是关于保持您的构建脚本声明性,以便充分利用Kotlin DSL

使用buildSrc项目是关于将您的构建逻辑组织成共享的本地插件和约定,这些插件和约定易于测试并提供良好的 IDE 支持。

依赖管理

常见依赖

// groovy
implementation project(':library')
implementation 'com.xxxx:xxxx:8.8.1'

// kotlin
implementation(project(":library"))
implementation("com.xxxx:xxx:8.8.1")

freeTree

// groovy
implementation fileTree(include: '*.jar', dir: 'libs')

//kotlin
implementation(fileTree(mapOf("include" to listOf("*.jar"), "dir" to "libs")))

特别类型库依赖

//groovy
implementation(name: 'splibrary', ext: 'aar')

//kotlin
implementation (group="",name="splibrary",ext = "aar")

构建变体

显式和隐式 buildTypes

在 Kotlin DSL 中,某些 buildTypes(如 debugrelease,)是隐式提供的。但是,其他 buildTypes 则必须手动创建。

例如,在 Groovy 中,您可能有 debugreleasestaging buildTypes

buildTypes
  debug {
    ...
  }
  release {
    ...
  }
  staging {
    ...
  }

在 KTS 中,仅 debugrelease buildTypes 是隐式提供的,而 staging 则必须由您手动创建:

buildTypes
  getByName("debug") {
    ...
  }
  getByName("release") {
    ...
  }
  create("staging") {
    ...
  }

举例说明

Grovvy编写:

productFlavors {
        demo {
            dimension "app"
        }
        full {
            dimension "app"
            multiDexEnabled true
        }
    }

buildTypes {
        release {
            signingConfig signingConfigs.signConfig
            minifyEnabled true
            debuggable false
            proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
        }

        debug {
            minifyEnabled false
            debuggable true
        }
    }
signingConfigs {
        release {
            storeFile file("myreleasekey.keystore")
            storePassword "password"
            keyAlias "MyReleaseKey"
            keyPassword "password"
        }
        debug {
            ...
        }
    }

kotlin-KTL编写:

productFlavors {
    create("demo") {
        dimension = "app"
    }
    create("full") {
        dimension = "app"
        multiDexEnabled = true
    }
}

buildTypes {
        getByName("release") {
            signingConfig = signingConfigs.getByName("release")
            isMinifyEnabled = true
            isDebuggable = false
            proguardFiles(getDefaultProguardFile("proguard-android.txtt"), "proguard-rules.pro")
        }
        
        getByName("debug") {
            isMinifyEnabled = false
            isDebuggable = true
        }
    }
    
signingConfigs {
        create("release") {
            storeFile = file("myreleasekey.keystore")
            storePassword = "password"
            keyAlias = "MyReleaseKey"
            keyPassword = "password"
        }
        getByName("debug") {
            ...
        }
    }

访问配置

gradle.properties

我们通常会把签名信息、版本信息等配置写在gradle.properties中,在kotlin-dsl中我们可以通过一下方式访问:

  1. rootProject.extra.properties
  2. project.extra.properties
  3. rootProject.properties
  4. properties
  5. System.getProperties()

System.getProperties()使用的限制比较多

  • 参数名必须按照systemProp.xxx格式(例如:systemProp.kotlinVersion=1.3.72);
  • 与当前执行的task有关(> Configure project :buildSrc> Configure project :的结果不同,后者无法获取的gradle.properties中的数据);

local.properties

获取工程的local.properties文件

gradleLocalProperties(rootDir)

gradleLocalProperties(projectDir)

获取系统环境变量的值

val JAVA_HOME:String = System.getenv("JAVA_HOME") ?: "default_value"

关于Ext

Google 官方推荐的一个 Gradle 配置最佳实践是在项目最外层 build.gradle 文件的ext代码块中定义项目范围的属性,然后在所有模块间共享这些属性,比如我们通常会这样存放依赖的版本号。

// build.gradle

ext {
    compileSdkVersion = 28
    buildToolsVersion = "28.0.3"
    supportLibVersion = "28.0.0"
    ...
}

但是由于缺乏IDE的辅助(跳转查看、全局重构等都不支持),实际使用体验欠佳。

KTL中用extra来代替Groovy中的ext

// The extra object can be used for custom properties and makes them available to all
// modules in the project.
// The following are only a few examples of the types of properties you can define.
extra["compileSdkVersion"] = 28
// You can also create properties to specify versions for dependencies.
// Having consistent versions between modules can avoid conflicts with behavior.
extra["supportLibVersion"] = "28.0.0"
android {
    // Use the following syntax to access properties you defined at the project level:
    // rootProject.extra["property_name"]
    compileSdkVersion(rootProject.extra["sdkVersion"])

    // Alternatively, you can access properties using a type safe delegate:
    val sdkVersion: Int by rootProject.extra
    ...
    compileSdkVersion(sdkVersion)
}
...
dependencies {
    implementation("com.android.support:appcompat-v7:${rootProject.ext.supportLibVersion}")
    ...
}

build.gralde中的ext数据是可以在build.gradle.kts中使用extra进行访问的。

修改生成apk名称和BuildConfig中添加apk支持的cpu架构

val abiCodes = mapOf("armeabi-v7a" to 1, "x86" to 2, "x86_64" to 3)
android.applicationVariants.all {
    val buildType = this.buildType.name
    val variant = this
    outputs.all {
        val name =
            this.filters.find { it.filterType == com.android.build.api.variant.FilterConfiguration.FilterType.ABI.name }?.identifier
        val baseAbiCode = abiCodes[name]
        if (baseAbiCode != null) {
            //写入cpu架构信息
            variant.buildConfigField("String", "CUP_ABI", "\"${name}\"")
        }
        if (this is com.android.build.gradle.internal.api.ApkVariantOutputImpl) {
            //修改apk名称
            if (buildType == "release") {
                this.outputFileName = "KotlinDSL_${name}_${buildType}.apk"
            } else if (buildType == "debug") {
                this.outputFileName = "KotlinDSL_V${variant.versionName}_${name}_${buildType}.apk"
            }
        }
    }
}

buildSrc

我们在使用Groovy语言构建的时候,往往会抽取一个version_config.gradle来作为全局的变量控制,而ext扩展函数则是必须要使用到的,而在我们的Gradle Kotlin DSL中,如果想要使用全局控制,则需要建议使用buildSrc

复杂的构建逻辑通常很适合作为自定义任务或二进制插件进行封装。自定义任务和插件实现不应存在于构建脚本中。buildSrc则不需要在多个独立项目之间共享代码,就可以非常方便地使用该代码了。

buildSrc被视为构建目录。编译器发现目录后,Gradle会自动编译并测试此代码,并将其放入构建脚本的类路径中。

  1. 先创建buildSrc目录;
  2. 在该目录下创建build.gradle.kts文件;
  3. 创建一个buildSrc/src/main/koltin目录;
  4. 在该目录下创建Dependencies.kt文件作为版本管理类;

需要注意的是buildSrcbuild.gradle.kts

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

或者

apply {
    plugin("kotlin")
}
buildscript {
    repositories {
        gradlePluginPortal()
    }
    dependencies {
        classpath(kotlin("gradle-plugin", "1.3.72"))
    }
}
//dependencies {
//    implementation(gradleKotlinDsl())
//    implementation(kotlin("stdlib", "1.3.72"))
//}
repositories {
    gradlePluginPortal()
}

不同版本之间buildSrc下的build.gradle文件执行顺序:

gradle-wrapper.properties:5.6.4

com.android.tools.build:gradle:3.2.0

  1. BuildSrc:build.gradle
  2. setting.gradle
  3. Project:build.gradle
  4. Moudle:build.gradle

gradle-wrapper.properties:6.5

com.android.tools.build:gradle:4.1.1

  1. setting.gradle
  2. BuildSrc:build.gradle
  3. Project:build.gradle
  4. Moudle:build.gradle

所以在非buildSrc目录下的build.gradle.kts文件中我们使用Dependencies.kt需要注意其加载顺序。

参考文档

Android官网-将构建配置从 Groovy 迁移到 KTS

Migrating build logic from Groovy to Kotlin

GitHub:kotlin-dsl-samples

GitHub:kotlin-dsl-samples/samples/hello-android

Kotlin DSL: Gradle scripts in Android made easy

buildSrc官方文档

Gradle’s Kotlin DSL BuildSrc

文章到这里就全部讲述完啦,若有其他需要交流的可以留言哦~!~!

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 205,874评论 6 479
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 88,151评论 2 382
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 152,270评论 0 342
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 55,137评论 1 278
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 64,116评论 5 370
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,935评论 1 283
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,261评论 3 399
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,895评论 0 259
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 43,342评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,854评论 2 323
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,978评论 1 333
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,609评论 4 322
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,181评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,182评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,402评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,376评论 2 352
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,677评论 2 344

推荐阅读更多精彩内容