前语
现在创立新的 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 依靠。
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 库房,假如对错 https 协议的地址,还需求增加
isAllowInsecureProtocol
的特点。 - 也能够直接增加本地库房的依靠。
setting.gradle 的内容本身就比较简略,搬迁时考虑以上内容即可。
project.build
再来来看整个项目的 build.gradle ,也便是根目录下的 build.gralde 。假如项目只有一个 moudle, 其实这个脚本中的内容能够合并到 module 的 build.gradle 中去。
运用 kotlin-dsl 时,这个脚本的定位就很单一了,仅有的作用便是生命整个项目用到了那些 gradle 插件。
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 插件也能够按如下方法声明
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 .
- 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 ,才真正完成了对这些插件的运用。
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 中咱们能够很容易的完成这个功用。
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 中依然是支撑的,只不过导入的语法有些改动。
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
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
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 中界说,完成动态注入,关于这部分的完成需求留意。
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
,需求按如下方法获取
val apiKey: String = project.findProperty("API_KEY") as String
signingConfig
另一个改变比较大的部分便是签名文件的装备,在 kotlin-dsl 默认存在 debug 类型的签名装备,release 或者是其他类型的需求咱们自己创立。
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 的装备。
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。
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 被废弃的话,也能够运用如下方法
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 的完成现在更便利了。
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")
}
}
可是也是修正其起来最让人头疼的地方,尤其是项目中依靠的三方库较多的话,一个个手动改实在是比较费力。能够借助以下脚本完成替换。
import re
def replace_implementation(file_path):
with open(file_path, 'r') as file:
content = file.read()
pattern = r'implementations+"([^"]*)"'
new_content = re.sub(pattern, r'implementation("1")', content)
pattern = r"implementations+'(.*?)'"
new_content = re.sub(pattern, r'implementation("1")', new_content)
pattern = r'apis+"([^"]*)"'
new_content = re.sub(pattern, r'api("1")', new_content)
pattern = r"apis+'(.*?)'"
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
文件声明依靠的库、插件的版本号之间的对应联系。
[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 中就能够直接声明为
implementation(libs.androidx.core.ktx)
声明里的短横线还必须变成点,这也太怪异了,看着更乱了。
和直接写
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
。经过这样的语法提示,能够让咱们更好的理解脚本中这些熟悉的意义,在出现问题是能够更便利的查看源码,在对应的源码中找到问题的原因和解决方法。