前语

现在创立新的 Android 工程,Android Studio 默认的模板已经运用 kotlin-dsl 替代 gradle 作为构建脚本了。

Android gralde 脚本搬迁到 Kotlin DSL

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 这个脚本,有两项功用

  • 声明构建脚本依靠的远程库房
  • 声明当时工程的依靠的模块

关于企业级别的项目,除了依靠官方库房的内容,必定有一些依靠是经过私有服务进行依靠的。因而,除了官方示例中提供的 mavenCentralgradlePluginPortal 之外,还需求咱们手动增加一些私有的 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 的方法导入。

Android gralde 脚本搬迁到 Kotlin DSL

这个特性在 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 gralde 脚本搬迁到 Kotlin DSL

比方这儿能够看到 android 这个 lambda 是在对 BaseAppModuleExtension 这个类的特点进行修正,以此类推 defaultConfig 是在修正 ApplicationDefaultConfig 。经过这样的语法提示,能够让咱们更好的理解脚本中这些熟悉的意义,在出现问题是能够更便利的查看源码,在对应的源码中找到问题的原因和解决方法。

参阅文档