本文为稀土掘金技术社区首发签约文章,30天内禁止转载,30天后未获授权禁止转载,侵权必究!
1、前言
目前Android开发中,构建脚本语言有两种,分别是Groovy DSL
和Kotlin DSL
。
而在前面的系列文章中,Gradle配置的示例代码都是使用Groovy编写的,也算是有意而为之吧,目的就是为这篇迁移文章做铺垫。
迁移之后两种语言的写法就都涉及到了,可以有更多的选择了,用的时候也不至于因为语法问题难道英雄汉...
2、优劣
Groovy DSL和Kotlin DSL相比,Groovy的运行性能
更好,但是Kotlin增加了一些代码补全、编译时检查等开发体验
较好的功能。面向开发者来说,显然Kotlin要更友好一些,牺牲一丢丢性能损耗也无伤大雅。
而且随着Kotlin的不断迭代和普及程度,不光在性能上会慢慢追平并超越,用的人也会越来越多。
所以,我认为从Groovy迁移至Kotlin还是很有必要的。
3、迁移
Groovy和Kotlin在构建脚本文件上的区别:
- Groovy编写的build文件是以
.gradle
为文件扩展名的,即build.gradle; - Kotlin编写的build文件是以
.gradle.kts
为文件扩展名的,即build.gradle.kts;
除此之外,剩下的就是语法上的区别,不过也不用担心,Gradle是支持混编的,而且本章会以我们常用的settings.gradle
、app:build.gradle
、rootProject:build.gradle
为实操,带领大家过一遍迁移细节,后面也可以直接拿来当模版用。
4、settings.gradle
settings.gradle位于项目的根目录下,是用来统一管理插件和依赖的下载地址以及解析策略,还有指定参与源码编译的项目,分别对应着pluginManagement、dependencyResolutionManagement和include/includeBuild。
将settings.gradle,重命名为settings.gradle.kts。
4.1、pluginManagement
Groovy:
ruby
pluginManagement {
repositories {
gradlePluginPortal()
google()
mavenCentral()
maven {
// 本地maven仓库地址
url './plugin/build/maven-repo'
}
maven { url 'https://jitpack.io' }
}
// 插件解析策略 useVersion/useModule
resolutionStrategy {
eachPlugin {
if (requested.id.id == "com.android.application") {
//useVersion("2.0")
}
}
}
}
Kotlin:
scss
pluginManagement {
repositories {
gradlePluginPortal()
google()
mavenCentral()
maven("https://jitpack.io")
// 本地maven仓库地址
maven("./plugin/build/maven-repo")
}
// 插件解析策略 useVersion/useModule
resolutionStrategy {
eachPlugin {
if (requested.id.id == "com.android.application") {
//useVersion("2.0")
}
}
}
}
这里主要是maven的写法差异。
将:
rust
maven { url 'https://jitpack.io' }
改为:
scss
maven("https://jitpack.io")
4.2、dependencyResolutionManagement
Groovy:
ruby
dependencyResolutionManagement {
repositoriesMode.set(RepositoriesMode.PREFER_SETTINGS)
repositories {
google()
mavenCentral()
maven {
// 本地maven仓库地址
url './plugin/build/maven-repo'
}
maven { url 'https://jitpack.io' }
// 阿里云仓库
maven { url "https://maven.aliyun.com/nexus/content/repositories/releases" }
maven { url 'https://maven.aliyun.com/repository/public/' }
flatDir {
dirs 'libs'
}
}
}
Kotlin:
scss
dependencyResolutionManagement {
repositoriesMode.set(RepositoriesMode.PREFER_SETTINGS)
repositories {
google()
mavenCentral()
maven("https://jitpack.io")
// 本地maven仓库地址
maven("./plugin/build/maven-repo")
// 阿里云仓库
maven("https://maven.aliyun.com/nexus/content/repositories/releases")
maven("https://maven.aliyun.com/repository/public/")
flatDir {
dirs("libs")
}
}
}
除了maven的写法差异外,dirs函数的调用必须得是括号,且字符串参数必须是双引号。
4.3、include
Groovy:
php
rootProject.name = "GradleX"
include ':app'
include ':plugin'
Kotlin:
php
rootProject.name = "GradleX"
include(":app")
include(":plugin")
同上,函数的调用必须得是括号,且字符串参数必须是双引号,includeBuild同理。
4.4、依赖plugin
在原来settings.gradke中有个源码和AAR切换的插件,这里为直接抽到一个UseLocalPlugin.gradle文件中。
typescript
apply plugin: UseLocalPlugin
class UseLocalPlugin implements Plugin<Settings> {
@Override
void apply(Settings settings) {
// ...
}
}
引用:
ini
apply(from = "UseLocalPlugin.gradle")
会自动解析并应用插件。
老的依赖方式:
csharp
apply from: 'UseLocalPlugin.gradle'
5、rootProject:build.gradle
位于项目的根目录下,通常用来定义全局的插件、仓库和配置。
将build.gradle重命名为build.gradle.kts。
5.1、buildscript
Groovy:
scss
buildscript {
repositories {
google()
mavenCentral()
}
dependencies {
// GradleX本地仓库地址
// classpath('com.yechaoa.plugin:gradleX:1.6-SNAPSHOT')
// GradleX远端仓库地址
classpath('com.github.yechaoa.GradleX:plugin:1.8')
}
}
Kotlin:
scss
buildscript {
repositories {
google()
mavenCentral()
}
dependencies {
// GradleX本地仓库地址
// classpath("com.yechaoa.plugin:gradleX:1.6-SNAPSHOT")
// GradleX远端仓库地址
classpath("com.github.yechaoa.GradleX:plugin:1.8")
}
}
这里只要把GradleX插件依赖的仓库地址从单引号改成双引号即可。
5.2、plugins
Groovy:
bash
plugins {
id("com.android.application") version '8.1.1' apply false
id 'com.android.library' version '8.1.1' apply false
id 'org.jetbrains.kotlin.android' version '1.7.10' apply false
id "com.dorongold.task-tree" version "2.1.1"
}
Kotlin:
bash
plugins {
id("com.android.application") version "8.1.1" apply false
id("com.android.library") version "8.1.1" apply false
id("org.jetbrains.kotlin.android") version "1.7.10" apply false
id("com.dorongold.task-tree") version "2.1.1"
}
这里主要是把plugin依赖的id调用改成括号,然后version的指定也改成双引号即可。
5.3、task
Groovy:
typescript
task clean(type: Delete) {
delete rootProject.buildDir
}
Kotlin:
scss
tasks.create<Delete>("clean") {
delete(rootProject.buildDir)
}
这个clean的task也可以不要,Clean Project
是一样的。
5.4、额外属性
Groovy:
ini
ext {
kotlinVersion = "1.9.20"
}
// or
ext.kotlinVersion = "1.9.20"
Kotlin:
kotlin
val kotlinVersion by extra("1.9.20")
6、app:build.gradle
位于每个 project /module/ 目录下,用于指定其模块的build设置,主要包括插件、项目配置和依赖。
将build.gradle重命名为build.gradle.kts。
6.1、plugins
Groovy:
bash
plugins {
id 'com.android.application'
id 'org.jetbrains.kotlin.android'
id 'com.yechaoa.plugin.gradleX'
}
Kotlin:
bash
plugins {
id("com.android.application")
id("org.jetbrains.kotlin.android")
id("com.yechaoa.plugin.gradleX")
}
使用括号和双引号。
另外,plugin的依赖方式有两种。
一种是名称直接引用name的方式,即
markdown
plugins {
gradleX
}
另一种就是id的方式,即
bash
plugins {
id("com.yechaoa.plugin.gradleX")
}
更推荐
大家使用id的方式依赖插件。
6.2、android
6.2.1、namespace
Groovy:
arduino
namespace 'com.yechaoa.gradlex'
Kotlin:
ini
namespace = "com.yechaoa.gradlex"
6.2.2、compileSdk
Groovy:
markdown
compileSdk 33
Kotlin:
ini
compileSdk = 33
6.2.3、defaultConfig
Groovy:
csharp
defaultConfig {
applicationId "com.yechaoa.gradlex"
minSdk 23
targetSdk 33
versionCode getVersionCodeByProperty()
versionName getVersionNameByProperty()
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
ndk {
//noinspection ChromeOsAbiSupport
abiFilters 'arm64-v8a', 'armeabi-v7a', 'armeabi-v8a'
}
}
Kotlin:
ini
defaultConfig {
applicationId = "com.yechaoa.gradlex"
minSdk = 23
targetSdk = 33
versionCode = getVersionCodeByProperty()
versionName = getVersionNameByProperty()
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
ndk {
//noinspection ChromeOsAbiSupport
abiFilters.addAll(mutableSetOf("arm64-v8a", "armeabi-v7a", "armeabi-v8a"))
}
}
6.2.4、buildTypes
Groovy:
java
buildTypes {
debug {
versionNameSuffix "-测试包"
}
release {
versionNameSuffix "-正式包"
minifyEnabled false
proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
}
}
Kotlin:
javascript
buildTypes {
getByName("debug") {
versionNameSuffix = "-测试包"
}
getByName("release") {
versionNameSuffix = "-正式包"
isMinifyEnabled = false
proguardFiles(getDefaultProguardFile("proguard-android-optimize.txt"), "proguard-rules.pro")
}
}
6.2.5、compileOptions
Groovy:
markdown
compileOptions {
sourceCompatibility JavaVersion.VERSION_17
targetCompatibility JavaVersion.VERSION_17
}
Kotlin:
ini
compileOptions {
sourceCompatibility = JavaVersion.VERSION_17
targetCompatibility = JavaVersion.VERSION_17
}
6.2.6、kotlinOptions
Groovy:
ini
kotlinOptions {
jvmTarget = '17'
}
Kotlin:
ini
kotlinOptions {
jvmTarget = "17"
}
6.2.7、applicationVariants
Groovy:
arduino
applicationVariants.configureEach { variant ->
variant.outputs.each { output ->
if (variant.buildType.name == "debug") {
output.versionNameOverride = "3.0"
output.versionCodeOverride = 3
}
// 动态删除清单文件里的权限
// output.processManifest.doLast {
// // 获取manifest文件
// def manifestOutFile = output.processResourcesProvider.get().getManifestFile()
// // 读取manifest文件
// def manifestContent = manifestOutFile.getText()
// // 删除指定权限
// manifestContent = manifestContent.replaceAll('android.permission.RECORD_AUDIO', 'android.permission.INTERNET')
// // 再写回manifest文件
// manifestOutFile.write(manifestContent)
// }
}
}
Kotlin:
kotlin
applicationVariants.configureEach {
val buildTypeName = this.buildType.name
this.outputs.all {
val output = this as com.android.build.gradle.api.BaseVariantOutput
// 动态修改VersionName和VersionCode
if (buildTypeName == "debug") {
val apkOutput = this as? com.android.build.gradle.api.ApkVariantOutput
apkOutput?.let {
it.versionCodeOverride = 3
it.versionNameOverride = "3.0"
}
}
// 动态删除清单文件里的权限
// output.processManifest.doLast {
// // 获取manifest文件
// val manifestOutFile = output.processResourcesProvider.get().getManifestFile()
// // 读取manifest文件
// var manifestContent = manifestOutFile.readText()
// // 删除指定权限
// manifestContent = manifestContent.replace("android.permission.RECORD_AUDIO", "android.permission.INTERNET")
// // 再写回manifest文件
// manifestOutFile.writeText(manifestContent)
// }
}
}
这里主要是我们在动态修改VersionName和VersionCode的时候要用到versionCodeOverride和versionNameOverride,这倆方法不在BaseVariantOutput中,所以转换成ApkVariantOutput。
另外,多层嵌套this指向不明确的时候
6.2.8、buildFeatures
还有之前未提到的buildFeatures。
Kotlin:
ini
buildFeatures {
buildConfig = true
viewBinding = true
}
6.3、dependencies
Groovy:
css
dependencies {
implementation fileTree(dir: 'libs', include: ['*.aar'])
// implementation project(':plugin')
implementation 'androidx.core:core-ktx:1.9.0'
}
Kotlin:
less
dependencies {
implementation(fileTree(mapOf("dir" to "libs", "include" to listOf("*.jar", "*.aar"))))
// implementation(project(":plugin"))
implementation("androidx.core:core-ktx:1.9.0")
}
6.4、resolutionStrategy
Groovy:
csharp
// 版本冲突解析策略
resolutionStrategy.eachDependency { DependencyResolveDetails details ->
def requested = details.requested
if (requested.group == 'com.squareup.okhttp3' && requested.name == 'okhttp') {
details.useVersion '4.10.0'
}
// 还可以使用startsWith通配,比如room-compiler、room-rxjava2、room-testing
// if (requested.group == 'androidx.room' && requested.name.startsWith('room')) {
// details.useVersion '2.5.0'
// }
}
Kotlin:
ini
resolutionStrategy.eachDependency {
val details = this as DependencyResolveDetails
val requested = details.requested
if (requested.group == "com.squareup.okhttp3" && requested.name == "okhttp") {
details.useVersion("4.10.0")
}
// 还可以使用startsWith通配,比如room-compiler、room-rxjava2、room-testing
// if (requested.group == "androidx.room" && requested.name.startsWith("room")) {
// details.useVersion "2.5.0"
// }
}
details的引用在写法上做了一层转换,比直接使用this的写法更易于阅读,也可以lambda指定类型。
或者也可以使用kotlin object匿名内部类的方式:
kotlin
resolutionStrategy.eachDependency(object :Action<DependencyResolveDetails>{
override fun execute(details: DependencyResolveDetails) {
val requested = details.requested
if (requested.group == "com.squareup.okhttp3" && requested.name == "okhttp") {
details.useVersion("4.10.0")
}
}
})
6.5、substitute
Groovy:
sql
resolutionStrategy.dependencySubstitution {
// 在local.properties中配置useLocal=true,或执行:./gradlew assembleDebug -PuseLocal=true
// if (useLocal) {
// // 把yutilskt:3.4.0替换成源码依赖
// substitute module('com.github.yechaoa.YUtils:yutilskt:3.4.0') using project(':yutilskt')
// }
// 把yutilskt替换成远端依赖
// substitute project(':yutilskt') using module('com.github.yechaoa.YUtils:yutilskt:3.4.0')
// 把yutilskt:3.3.3替换成yutilskt:3.4.0
// substitute module('com.github.yechaoa.YUtils:yutilskt:3.3.3') using module('com.github.yechaoa.YUtils:yutilskt:3.4.0')
}
Kotlin:
sql
resolutionStrategy.dependencySubstitution {
// 在local.properties中配置useLocal=true,或执行:./gradlew assembleDebug -PuseLocal=true
// if (useLocal) {
// // 把yutilskt:3.4.0替换成源码依赖
// substitute(module("com.github.yechaoa.YUtils:yutilskt:3.4.0")).using(project(":yutilskt"))
// }
// 把yutilskt替换成远端依赖
// substitute(project (":yutilskt")).using(module("com.github.yechaoa.YUtils:yutilskt:3.4.0"))
// 把yutilskt:3.3.3替换成yutilskt:3.4.0
// substitute(module("com.github.yechaoa.YUtils:yutilskt:3.3.3")).using(module("com.github.yechaoa.YUtils:yutilskt:3.4.0"))
}
在kotlin dsl中substitute的写法要连着写,即
less
substitute(module("com.github.yechaoa.YUtils:yutilskt:3.4.0")).using(project(":yutilskt"))
6.6、localProperties
Groovy:
scss
// 加载local.properties文件
def localProperties = new Properties()
file("../local.properties").withInputStream { localProperties.load(it) }
// 从localProperties中获取useLocal属性
def useLocal = localProperties.getProperty('useLocal', 'false').toBoolean()
Kotlin:
scss
// 加载local.properties文件
val localProperties = Properties()
file("../local.properties").inputStream().use { localProperties.load(it) }
// 从localProperties中获取useLocal属性
val useLocal = localProperties.getProperty("useLocal", "false").toBoolean()
7、总结
以上就是从Groovy迁移至Kotlin的主要流程了,更多的大家可以查看github源码。
我们来总结一下迁移过程中主要的几个语法差异:
字符引号
:Groovy中字符串可以用单引号'string'或双引号来"string",但是在Kotlin中则必须使用双引号。函数调用
:Groovy允许在调用函数时省略括号,而Kotlin则是必须显式使用括号。属性赋值
:比如=
号,Groovy允许省略赋值运算符,而Kotlin则必须显式使用赋值运算符。额外属性
:Groovy的额外属性是使用ext对象,Kotlin中是使用extra对象。