Android gralde 脚本迁移到 Kotlin DSL

前言

现在创建新的 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 这个脚本,有两项功能

  • 声明构建脚本依赖的远程仓库
  • 声明当前工程的依赖的模块

对于企业级别的项目,除了依赖官方仓库的内容,必然有一些依赖是通过私有服务进行依赖的。因此,除了官方示例中提供的 mavenCentralgradlePluginPortal 之外,还需要我们手动添加一些私有的 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 。通过这样的语法提示,可以让我们更好的理解脚本中这些熟悉的含义,在出现问题是可以更方便的查看源码,在对应的源码中找到问题的原因和解决方法。

参考文档

相关推荐
大白要努力!1 小时前
Android opencv使用Core.hconcat 进行图像拼接
android·opencv
天空中的野鸟2 小时前
Android音频采集
android·音视频
小白也想学C3 小时前
Android 功耗分析(底层篇)
android·功耗
曙曙学编程4 小时前
初级数据结构——树
android·java·数据结构
闲暇部落6 小时前
‌Kotlin中的?.和!!主要区别
android·开发语言·kotlin
诸神黄昏EX8 小时前
Android 分区相关介绍
android
大白要努力!9 小时前
android 使用SQLiteOpenHelper 如何优化数据库的性能
android·数据库·oracle
Estar.Lee9 小时前
时间操作[取当前北京时间]免费API接口教程
android·网络·后端·网络协议·tcp/ip
Winston Wood9 小时前
Perfetto学习大全
android·性能优化·perfetto
Dnelic-12 小时前
【单元测试】【Android】JUnit 4 和 JUnit 5 的差异记录
android·junit·单元测试·android studio·自学笔记