前言
现在创建新的 Android 工程,Android Studio 默认的模板已经使用 kotlin-dsl 取代 gradle 作为构建脚本了。
kotlin-dsl 脚本相对于以往的 gradle 脚本,最大的优势莫过于良好的代码提示了。下面总结一下旧项目 gradle 脚本迁移到 kotlin-dsl 的一些心得和用法技巧。
kotlin-dsl 和 gradle 的语法实现,有些地方还是非常相似的,无非就是多个括号,加个等于号,把单引号改成双引号 就能轻松搞定的变更。
比如以下内容
变更项 | gradle | kotlin-dsl |
---|---|---|
setting 中配置 project | include ':app' |
include(":app") |
项目依赖 | implementation 'com.squareup.okio:okio:3.9.0' |
implementation("com.squareup.okhttp3:okhttp:4.9.0") |
需要添加 = |
namespace 'com.engineer.android.mini' |
namespace = "com.engineer.android.mini" |
对于此类可以照猫画虎实现的内容,不再赘述,主要记录一些变更语法较大,无法简单实现的逻辑。
gralde to kotlin-dsl
这里需要注意的是,虽然构建脚本从 gradle 变成了 kotlin-dsl ,但是构建流程依然是 gradle 那一套,并没有发生改变。从广义的角度出发,可以认为是构建流程的配置文件类型变了,但是构建的整个流程没有变化
下面就从 gradle 构建的生命周期出发,从外向内一步步阐释从 gradle 脚本迁移到 kotlin-dsl 时的注意事项。
project-setting
对于 setting.gradle.kts 这个脚本,有两项功能
- 声明构建脚本依赖的远程仓库
- 声明当前工程的依赖的模块
对于企业级别的项目,除了依赖官方仓库的内容,必然有一些依赖是通过私有服务进行依赖的。因此,除了官方示例中提供的 mavenCentral
和 gradlePluginPortal
之外,还需要我们手动添加一些私有的 maven 依赖。
kts
dependencyResolutionManagement {
repositoriesMode.set(RepositoriesMode.FAIL_ON_PROJECT_REPOS)
repositories {
google()
mavenCentral()
// 添加 jitpack 的依赖
maven { url = uri("https://jitpack.io") }
// 添加 私有仓库的依赖
maven {
this.isAllowInsecureProtocol = true
url = uri("http://192.168.11.112")
}
// 添加本地仓库的依赖
maven { url = uri("${rootDir}/local_repo/") }
}
}
- 当年由于 Jcenter 的停服事件,很多开源库迁移到了 jitpack ,同时 jitpack 本身也有很多个人开发者维护的仓库。因此,势必需要单独添加 jitpack 的依赖。
- 对于内部私有的 maven 仓库,如果是非 http 协议的地址,还需要添加
isAllowInsecureProtocol
的属性。 - 也可以直接添加本地仓库的依赖。
setting.gradle 的内容本身就比较简单,迁移时考虑以上内容即可。
project.build
再来来看整个项目的 build.gradle ,也就是根目录下的 build.gralde 。如果项目只有一个 moudle, 其实这个脚本中的内容可以合并到 module 的 build.gradle 中去。
使用 kotlin-dsl 时,这个脚本的定位就很单一了,唯一的作用就是生命整个项目用到了那些 gradle 插件。
kotlin
plugins {
alias(libs.plugins.android.application) apply false
alias(libs.plugins.android.library) apply false
alias(libs.plugins.jetbrains.kotlin.android) apply false
alias(libs.plugins.hilt.android) apply false
alias(libs.plugins.tools.ksp) apply false
}
这部分内容,按照自身的需要直接添加就可以了。当然,上面使用到了 catalog,如果你不想用的话,也可以直接写插件的 id 和 版本号,比如最后的 ksp 插件也可以按如下方式声明
kotlin
id("com.google.devtools.ksp") version("1.9.0-1.0.13") apply false
apply false
这里再说一下 apply false
是什么意思?初次看到这个语法,感觉很奇怪,这个插件到底是用还是不用呢 ? 其实这里搞清楚 gradle 中 project 的定义就明白了。对于一个由 gradle 构建的项目来说,是一个大的 project 里包含了多个独立或者有相互依赖关系的 project, 而这些子 project 就是通过 setting.gradle.kts 中通过 include("xxx") 声明的 module,每一个 module 就是一个 project .
shell
- project ("MinApp")
- project ("app")
- project ("common")
- project ("compose")
类似上面这样的结构,而这里的 build.gralde.kts 是属于 MinApp 这个 project 的。我们声明 android-applicatin
,android-library
,kotlin
,hilt
,ksp
这些插件并不是要用于 MinApp 这个 project,而是要用于他下面的这些子 module,因此这里用 apply false
的含义就是这个。 在子 module 中,我们通过 apply 插件的 id ,才真正实现了对这些插件的使用。
kotlin
plugins {
alias(libs.plugins.android.application)
alias(libs.plugins.jetbrains.kotlin.android)
alias(libs.plugins.hilt.android)
alias(libs.plugins.tools.ksp)
}
因此,可以认为根目录下的 build.gradle.kts 是用来声明一些所有模块都要用到的组件。
module.build.gradle
下面重点说一下,平时最最常用的 module 的 build.gradle 的变更。
android 配置
对于配置脚本中 android {}
这个模块的配置,其实就是在给 BaseAppModuleExtension
这个类的各种属性赋值。因此改动最多的点,就是给所有的写操作添加 =
等于号。
这里举一个动态修改版本号的例子。在某些项目中,为了方便追踪问题,会在打包时动态修改 versionName 字段的值,添加构建时间和commitID。在 kotlin-dsl 中我们可以很容易的实现这个功能。
kotlin
val buildTime: String = SimpleDateFormat("yyMMddHHmm").format(Date())
android {
defaultConfig {
applicationId = "com.engineer.android.mini"
versionName = "1.0.0_$buildTime"
}
}
这里简单起见以添加时间为例,定义了一个获取时间的方法,在给 versionName 字段赋值时在原先版本号的基础上追加这个内容即可。
对于赋值这部分的修改,需要注意的是,在 Kotlin 中单引号是字符,双引号是字符串,因此对于取值为字符串的内容,都要将原先的单引号修改为双引号。
apply
可以看到将 gradle 脚本修改为 kotlin-dsl 还是挺繁琐的,不过有个好消息。
kotlin-dsl 是支持 gradle 脚本的
在之前的 gradle 实用技巧 一文中我们说过可以将一些常用的 gradle 脚本封装成模块化的单一文件,然后通过 apply file 的形式导入。
这个特性在 kotlin-dsl 中依然是支持的,只不过导入的语法有些变更。
kotlin
apply(from = "../custom-gradle/test-dep.gradle")
apply(from = "../custom-gradle/viewmodel-dep.gradle")
apply(from = "../custom-gradle/coroutines-dep.gradle")
apply(from = "../custom-gradle/rx-retrofit-dep.gradle")
apply(from = "../custom-gradle/hilt-dep.gradle")
apply(from = "../custom-gradle/apk_dest_dir_change.gradle")
apply(from = "../custom-gradle/report_apk_size_after_package.gradle")
这里 custom-gradle 目录下的脚本可以包含各种实现。比如 coroutines-dep.gradle
gradle
dependencies {
// coroutines
// 👇 依赖协程核心库
implementation "org.jetbrains.kotlinx:kotlinx-coroutines-core:1.7.3"
// 👇 依赖当前平台所对应的平台库
implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-android:1.7.3'
}
再比如 check-style.gradle
gradle
apply plugin: 'checkstyle'
task checkstyle(type: Checkstyle) {
source 'src/main/java'
exclude 'src/main/assets/'
exclude '**/gen/**'
exclude '**/test/**'
exclude '**/androidTest/**'
configFile new File(rootDir, "checkstyle.xml")
classpath = files()
}
因此,在从 gradle 迁移到 kotlin-dsl 的过程中
- 对于已有的模块化内容是可以直接复用的。没有必要为了仅仅语法的变更而去把功能再实现一遍。
- 再有,在实际迁移的过程中,对于编译报错的部分,可以先抽离为单独的 gradle 文件,通过在 kotlin-dsl 中 apply 的方式逐步解决,避免被海量的报错信息劝退。
buildConfig 和 ManifestPlaceHolder
我们可以在 BuildConfig 中自定义属性,方便在运行时基于编译内容做一些差异化的逻辑。在 AndroidManifest.xml 中有些内容(比如sdk 的 appkey) 等,也可以通过在 gradle 中定义,实现动态注入,对于这部分的实现需要注意。
kotlin
buildConfigField("Boolean", "enable_log", "false")
buildConfigField("String", "secret_id", "\"123456\"")
buildConfigField("String", "api_key", "\"${apiKey}\"")
manifestPlaceholders["max_aspect"] = 3
manifestPlaceholders["extract_native_libs"] = true
manifestPlaceholders["activity_exported"] = true
在 gradle 脚本中,我们可以直接读取定义在 gradle.properties 文件中的值,在 kotlin-dsl 中需要我们按照键值进行读取,比如上面的 apiKey
,需要按如下方式获取
kotlin
val apiKey: String = project.findProperty("API_KEY") as String
signingConfig
另一个变化比较大的部分就是签名文件的配置,在 kotlin-dsl 默认存在 debug 类型的签名配置,release 或者是其他类型的需要我们自己创建。
kotlin
signingConfigs {
create("release") {
storeFile = file(project.findProperty("MYAPP_RELEASE_STORE_FILE") as String)
storePassword = project.findProperty("MYAPP_RELEASE_STORE_PASSWORD") as String
keyAlias = project.findProperty("MYAPP_RELEASE_KEY_ALIAS") as String
keyPassword = project.findProperty("MYAPP_RELEASE_KEY_PASSWORD") as String
}
}
buildTypes {
release {
isMinifyEnabled = true
signingConfig = signingConfigs.findByName("release")
isShrinkResources = true
proguardFiles(getDefaultProguardFile("proguard-android-optimize.txt"), "proguard-rules.pro")
}
}
这里需要注意的是 release 这个lambda 表达式中,有些字段的属性名发生了变化,命名风格更符合 kotlin 。
flavor 配置
再看一下使用较多的 flavor 的配置。
kotlin
flavorDimensions.add("channel")
flavorDimensions.add("type")
productFlavors {
create("xiaomi") { dimension = "channel" }
create("oppo") { dimension = "channel" }
create("huawei") { dimension = "channel" }
create("local") { dimension = "type" }
create("global") { dimension = "type" }
}
这部分总的来说变化不大,无非是需要动态创建 flavor ,在 lambda 表达式中,依然可以像之前一样配置 applicationId 的后缀,实现不同的资源配置等逻辑。
再有一点就是关于 flavor 的配置,按照上述配置最终会有 3x2x2 = 12
种 flavor,无形中添加了很多不必要的 flavor,因此我们可以过滤掉某些非需要的 flavor。
kotlin
variantFilter {
println("***************************")
val flavorChannel = flavors.find { it.dimension == "channel" }?.name
val flavorType = flavors.find { it.dimension == "type" }?.name
println("flavor=$flavorChannel,type=$flavorType")
if (flavorChannel == "huawei" && flavorType == "global") {
ignore = true
}
if (flavorChannel == "xiaomi" && flavorType == "local") {
ignore = true
}
}
如果 variantFilter 被废弃的话,也可以使用如下方式
kotlin
androidComponents {
beforeVariants { variantBuilder ->
val flavorChannel = variantBuilder.productFlavors.find {
it.first == "channel"
}?.second
val flavorType = variantBuilder.productFlavors.find {
it.first == "type"
}?.second
if (flavorChannel == "oppo" && flavorType == "global") {
variantBuilder.enable = false
}
}
}
这样就可以灵活配置哪些 flavor 是生效的了,避免在构建流程中存在一大堆没有意义的 flavor。
dependencies
最后,就剩 dependencies 的配置了。这一部分比较枯燥,纯粹就是语法变更, 包括 exclude 的实现现在更方便了。
gradle
dependencies {
compileOnly(fileTree(mapOf("dir" to "libs", "include" to listOf("*.aar"))))
implementation(fileTree(mapOf("dir" to "libs", "include" to listOf("*.jar"))))
compileOnly("com.squareup.radiography:radiography:2.6")
implementation("androidx.appcompat:appcompat:1.6.1")
api("com.google.android.material:material:1.12.0")
implementation("com.facebook.fresco:fresco:3.1.3") {
exclude("com.facebook.soloader","soloader")
exclude("com.facebook.fresco","soloader")
exclude("com.facebook.fresco","soloader")
exclude("com.facebook.fresco","nativeimagefilters")
exclude("com.facebook.fresco","memory-type-native")
exclude("com.facebook.fresco","imagepipeline-native")
}
}
但是也是修改其起来最让人头疼的地方,尤其是项目中依赖的三方库较多的话,一个个手动改实在是比较费劲。可以借助以下脚本实现替换。
python
import re
def replace_implementation(file_path):
with open(file_path, 'r') as file:
content = file.read()
pattern = r'implementation\s+"([^"]*)"'
new_content = re.sub(pattern, r'implementation("\1")', content)
pattern = r"implementation\s+'(.*?)'"
new_content = re.sub(pattern, r'implementation("\1")', new_content)
pattern = r'api\s+"([^"]*)"'
new_content = re.sub(pattern, r'api("\1")', new_content)
pattern = r"api\s+'(.*?)'"
new_content = re.sub(pattern, r'api("\1")', new_content)
print(new_content)
if __name__ == '__main__':
target_file ="../custom-gradle/coroutines-dep.gradle"
replace_implementation(target_file)
通过正则表达式匹配内容,实现 impletation 'xxx' 到 impletataion("xxx") 的快速替换。
catalog
最后再说一下 catalog. 当我们把 dependencies 内的依赖替换完成之后,Android Studio 会提示我们用 catalog 替代。
catalog 就是在 gradle 这个目录下通过 libs.versions.toml
这样一个 toml
文件声明依赖的库、插件的版本号之间的对应关系。
toml
[versions]
agp = "8.4.0"
kotlin = "1.9.0"
coreKtx = "1.13.1"
hilt = "2.51"
ksp = "1.9.0-1.0.13"
# @keep
minSdk = "21"
targetSdk = "34"
compileSdk = "34"
[libraries]
androidx-core-ktx = { group = "androidx.core", name = "core-ktx", version.ref = "coreKtx" }
[plugins]
android-application = { id = "com.android.application", version.ref = "agp" }
android-library = { id = "com.android.library", version.ref = "agp" }
jetbrains-kotlin-android = { id = "org.jetbrains.kotlin.android", version.ref = "kotlin" }
hilt-android = { id = "com.google.dagger.hilt.android", version.ref = "hilt" }
tools-ksp = { id = "com.google.devtools.ksp", version.ref = "ksp" }
比如这里 libraries 下声明的 androidx-core-ktx ,在 dependencies 中就可以直接声明为
gradle
implementation(libs.androidx.core.ktx)
声明里的短横线还必须变成点,这也太诡异了,看着更乱了。
和直接写
kotlin
implementation("androidx.core:core-ktx:1.13.1")
没有任何区别。
所以,catalog 的使用见仁见智吧。使用直接写版本号的方式,感觉更清楚一些。
更多代码细节可以参考 MinApp
小结
Gradle 构建脚本从使用 groovy 语法的 .gradle
迁移到使用 kotlin 语法的 kotlin-dsl 还是能带来一些好处的,kotlin-dsl 的 lambda 在 Android Studio 中会有语法提示,同时会展示当前修改的是哪个类。
比如这里可以看到 android
这个 lambda 是在对 BaseAppModuleExtension
这个类的属性进行修改,以此类推 defaultConfig
是在修改 ApplicationDefaultConfig
。通过这样的语法提示,可以让我们更好的理解脚本中这些熟悉的含义,在出现问题是可以更方便的查看源码,在对应的源码中找到问题的原因和解决方法。