本文主要分享了 Android gradle 插件升级和 kts 迁移的相关知识和踩坑点。有个前置知识是依赖管理,依赖管理主要使用了 version catalog,之前写过一篇相关的文章,可以先熟悉下: Android 依赖管理及通用项目配置插件 谷歌和 gradle 官方都有相关的基础教程,如果你的项目要升级的话最好先详细阅读下 将构建配置从 Groovy 迁移到 KTS | Android 开发者 | Android DevelopersGradle Kotlin DSL PrimerMigrating build logic from Groovy to Kotlin。本文对应的 gradle 版本是 7.5.1,AGP(Android Gradle plugin)版本是 7.3.0,gradle 版本直接看 gralde 文档 即可,AGP 的最新版本可以参考 Android Gradle plugin API reference | Android Developers

为了让大家对迁移有个整体的认识,我画了个思维导图。

在开始迁移之前,需要注意在 sync 失败的情况下,kts 文件是没有代码提示的,此时迁移老代码会非常困难,所以强烈建议新建一个 demo 工程,或者也可以用 Android 依赖管理及通用项目配置插件 中的 demo 工程,这个工程的编译脚本都是 kts 文件,对照你需要修改的 groovy 代码,可以先在 demo 工程中用 kts 写一遍,然后复制到主工程中。 如果主工程部分脚本代码阻碍了 sync 成功导致没有代码提示,可以先注释掉让 sync 通过,然后再打开注释修改。

1 迁移 settings.gradle

首先将 settings.gradle 重命名为 settings.gradle.kts,然后写插件脚本:

@file:Suppress("UnstableApiUsage")

enableFeaturePreview("VERSION_CATALOGS")

enableFeaturePreview("TYPESAFE_PROJECT_ACCESSORS")

pluginManagement {
    repositories {
        gradlePluginPortal()
        mavenCentral()
        maven { setUrl("https://jitpack.io") }
    }
    resolutionStrategy {
        eachPlugin {
            when (requested.id.id) {
                "com.jady.lib.config-plugin" -> {
                    useModule("com.github.Jadyli.composing:config-plugin:0.1.8")
                }
            }
        }
    }
}

version catelog 并不是一个稳定版的功能,所以开头需要加几行代码,开启功能,抑制错误警告。

接着写依赖脚本:

dependencyResolutionManagement {
    repositoriesMode.set(RepositoriesMode.FAIL_ON_PROJECT_REPOS)
    repositories {
        google()
        mavenCentral()
        maven { setUrl("https://jitpack.io") }
    }
    versionCatalogs {
        create("commonLibs") { from(files("${rootDir.path}/.config/dependencies-common.toml")) }
        create("bizLibs") { from(files("${rootDir.path}/.config/dependencies-biz.toml")) }
    }
}

项目里所有的依赖从这里定义的仓库中解析。versionCatalogs 块中创建对应你 toml 文件的变量,你可以把自己公司的库放到 bizLibs 里,第三方库放到 commonLibs 中,方便区分,怎么安排随你喜欢。

其他的不多说,参考配置插件的 demo 工程吧。

2 迁移根目录 build.gradle

2.1 模块通用 properties

先配置通用配置插件所需的 properties:

 
ext {
    set("minSdk", bizLibs.versions.minSdk.get())
    set("targetSdk", bizLibs.versions.targetSdk.get())
    set("compileSdk", commonLibs.versions.compileSdk.get())
    set("javaMajor", commonLibs.versions.java.major.get())
    set("javaVersion", commonLibs.versions.java.asProvider().get())
    set("vectorDrawableSupportLibrary", true)
}

在设置通用 properties 的时候可能会需要读 local.properties,最新版的 gradle 好像没有直接的方法读取了,这里提供个:

fun gradleLocalProperties(projectRootDir: File): Properties {
    val properties = Properties()
    val localProperties = File(projectRootDir, "local.properties")

    if (localProperties.isFile) {
        InputStreamReader(FileInputStream(localProperties), Charsets.UTF_8).use { reader ->
            properties.load(reader)
        }
    } else {
        println("Gradle local properties file not found at $localProperties")
    }
    return properties
}

也有可能需要读取 yaml 文件,可以使用 yamlkt:

import net.mamoe.yamlkt.Yaml.Default
import net.mamoe.yamlkt.YamlList
import net.mamoe.yamlkt.YamlMap

buildscript {
    repositories {
        maven { setUrl("https://nexus.bilibili.co/content/groups/carbon/") }
    }

    dependencies {
        classpath("net.mamoe.yamlkt:yamlkt:0.12.0")
    }
}

/**
 * 版本号、版本名
 * module:
 * - APP_VERSION_NAME: "1.0.1"
 *   APP_VERSION_CODE: 1000100
 */
tasks.create("getVersionInfo") {
    // 读取 .module.yaml 来获取版本信息
    val yamlElement = Default.decodeYamlFromString(
        FileInputStream(File("${rootDir.path}/.module.yaml")).use { it.reader().readText() }
    )
    val properties = ((yamlElement as YamlMap).get("module") as YamlList).get(0) as YamlMap
    ext.run {
        set("versionName", properties.get("APP_VERSION_NAME")?.content?.toString())
        set("versionCode", (properties.get("APP_VERSION_CODE")?.content as? String)?.toInt())
    }
}
2.2 插件

文件开头需要加上 @file:Suppress("UnstableApiUsage") 以抑制错误,plugins 上面要加上 @Suppress("DSL_SCOPE_VIOLATION") 抑制 version catalog 的错误。

plugins {
    alias(commonLibs.plugins.spotless) apply false
    alias(commonLibs.plugins.android.application) apply false
    alias(commonLibs.plugins.android.library) apply false
    alias(commonLibs.plugins.kotlin.android) apply false
    alias(commonLibs.plugins.kotlin.kapt) apply false
    alias(commonLibs.plugins.config.plugin) apply false
    alias(commonLibs.plugins.tinker) apply false
}

可以发现我这里都没有 apply 到当前模块,那是因为根模块不需要,但是子模块内使用 apply(plugin: "***Id") 的形式是没法指定插件版本的,只能在根模块加上,理论上是应用从 settings.gradle.kts 的 pluginManagement 中找的,但是实际上并没有,所以需要有其中一个模块声明一下版本, 这里就用根模块了,可能是当前版本的 bug,希望后续能修复吧。如果你使用 apply(plugin: "***Id") 形式的时候遇到什么 id 找不到的错误,多半就是根模块的 build.gradle.kts 里的 plugins 没加上这个插件。

2.3 子模块
// 文件开头要导包
import com.diffplug.gradle.spotless.SpotlessExtension

val libs = commonLibs
subprojects {
    apply(plugin = libs.plugins.config.plugin.get().pluginId)
    if (name == "app") {
        apply(plugin = libs.plugins.android.application.get().pluginId)
    } else {
        apply(plugin = libs.plugins.android.library.get().pluginId)
    }
    apply(plugin = libs.plugins.spotless.get().pluginId)

    configure<SpotlessExtension> {

        format("misc") {
            target("*.md", ".gitignore")
            trimTrailingWhitespace()
            endWithNewline()
        }

        kotlin {
            target("**/*.kt")
            targetExclude(
                "**/copyright.kt",
            )
            ktlint("0.47.1")
                .setUseExperimental(true)
            trimTrailingWhitespace()
            endWithNewline()
        }

        kotlinGradle {
            target("**/*.kts")
            ktlint("0.47.1")
                .setUseExperimental(true)
            trimTrailingWhitespace()
            endWithNewline()
        }
    }
}

这里直接把 applicationlibrary 插件都统一配置了,你的项目可以按照实际情况来做。kotlin 代码格式化插件按需选吧,除了 ktlint 外,还有 ktfmtdiktat,但是后面两种虽然优点很多,但是问题也不少,比如 ktfmt 对于 ?: 的换行非常丑,而且由于是强制格式化的,所有人的代码风格会被强制统一,所以只能接受这种丑,而且官方也没有改的意思,还有最大行宽度默认是 120,调整功能好像也失效了,另外 indent 默认设成 2 了,虽然支持修改,但是只有 dropbox 风格的支持,各种问题下,我选择放弃。diktat 大家可以尝试下,它的默人要求 boolean 值要用 is/has 开头让我也有点没法接口,众所周知,java 调用 kotlin 返回 boolean 值的非 @JvmField 型 var 变量或者方法是会默认加上 is 的。所以,我最终还是推荐 ktlint,插件的话,最好还是用 spotless,确实不错。

3 迁移子模块 build.gradle

3.1 plugins

如果没有业务插件要配置的话,那就不用写了。这里以 parcelize 和 dokit  为例。

@file:Suppress("UnstableApiUsage")

@Suppress("DSL_SCOPE_VIOLATION")
plugins {
    id(commonLibs.plugins.kotlin.parcelize.get().pluginId)
    alias(bizLibs.plugins.dokit.plugin)
}

你可能会问,这个 parcelize 为什么要用 id() 且不带版本号的形式,那是因为如果用 alias,那 as 会报错:

Error resolving plugin [id: 'org.jetbrains.kotlin.plugin.parcelize', version: '1.7.10']
> The request for this plugin could not be satisfied because the plugin is already on the classpath with an unknown version, so compatibility cannot be checked.

其他插件类比,遇到这个错,直接换成 id 的形式就行了。

3.2 android 扩展属性配置
3.2.1 buildFeatures
buildFeatures {
    viewBinding = true
    // Determines whether to generate a BuildConfig class.
    buildConfig = true
    // Determines whether to support Data Binding.
    // Note that the dataBinding.enabled property is now deprecated.
    dataBinding = false
    // Determines whether to generate binder classes for your AIDL files.
    aidl = true
    // Determines whether to support RenderScript.
    renderScript = true
    // Determines whether to support injecting custom variables into the module’s R class.
    resValues = true
    // Determines whether to support shader AOT compilation.
    shaders = true
}

buildFeatures 内按需开启吧,注意 AGP 8.0 开始 renderScript默认改成 false了。

3.2.2 改输出包名

看代码就行了,经验,其中心酸懂的都懂,得出下面的代码挺艰难的。

applicationVariants.all(
    object : Action<com.android.build.gradle.api.ApplicationVariant> {
        override fun execute(variant: com.android.build.gradle.api.ApplicationVariant) {
            println("variant: $variant")
            variant.outputs.all(
                object : Action<com.android.build.gradle.api.BaseVariantOutput> {
                    override fun execute(
                        output: com.android.build.gradle.api.BaseVariantOutput
                    ) {
                        val outputImpl = output as com.android.build.gradle.internal.api.BaseVariantOutputImpl
                        val fileName = "xxx"
                        println("output file name: $fileName")
                        outputImpl.outputFileName = fileName
                    }
                }
            )
        }
    }
)
3.2.3 defaultConfig

只列举部分比较艰难的。 简单的自己加就好了。本文会举个生成主 app 共存版(主要用于开发的时候对比线上功能)的一个例子。

manifestPlaceholders.putAll(
    arrayOf(
        "key" to "value"
    )
)

sourceSets {
    getByName("basic") {
        res.srcDirs("src/main/res-night")
        getByName("debug") { java.srcDirs("src/debug/kotlin") }
    }
    maybeCreate("coexist")
    getByName("coexist") {
        // coexist 共用 debug 包代码
        java.srcDirs("src/debug/kotlin")
        res.srcDirs("src/coexist/res", "src/debug/res")
    }
}

basiccoexist 是两个 flavor,basic 是主 app,coexist 是共存版,你可能会疑惑为什么要叫 basic 呢?这里先不解释,等下面看到 flavor 就懂了。coexist 由于没有啥特殊逻辑,所以这里共用了一下 debug 包的代码。对于资源文件夹,由于在插件里已经加入了 "src/main/res",所以这里只需要加业务上特殊的就行了,大家按自己的项目来。

3.2.4 flavor

这里建立两个 flavor:

flavorDimensions.add("test")

productFlavors {
    create("basic") { signingConfig = signingConfigs["basic"] }
    create("coexist") {
        applicationIdSuffix = ".coexist"
        signingConfig = signingConfigs["coexist"]
    }
}

Android Studio 打包会按 flavor name 字母序来选择默认的 build variant,如果没有在 Build Variants tab 里手动选择过 build variant,as 会默认使用字母序最前的 build variat 来打包。所以主 app 的 flavor 名字取了个字母序比 coexist前的单词。另外这里还修改了一下 coexitst 的 applicationId。

3.2.5 signingConfigs
signingConfigs {
    create("basic") {
        storeFile = file("./test.keystore")
        storePassword = "test"
        keyAlias = "test.keystore"
        keyPassword = "test"
    }

    create("coexist") {
        storeFile = file("./test_coexist.keystore")
        storePassword = "test"
        keyAlias = "test_coexist.keystore"
        keyPassword = "test"
    }
}
3.2.6 namespace

目前,包名已经不是写在 manifest 里了,换成了 namespace 放在模块的 android 扩展属性下。

namespace = "com.jady"

3.3 dependencies

dependencies {
    implementation(fileTree("libs").include("*.jar", "*.aar"))
    implementation(projects.framework.utils)

    kapt(commonLibs.epoxy.processor)

    debugImplementation(bizLibs.dokit) { exclude(group = "com.google.zxing", module = "core") }
    implementation(commonLibs.androidx.annotation)

    // 版本号带有 @aar 形式的依赖比较特殊,需要按如下方法写
    api(bizLibs.***.library) {
        artifact {
            classifier = "release"
            type = "aar"
        }
    }

    // testing
    testImplementation(commonLibs.junit)
}

基本上大部分可能用到的场景上面的代码都有了,按需使用即可。toml 里如果有存在父子级形式的依赖,在 dependencies 里可以不用 asProvider,比如:

retrofit = { module = "com.squareup.retrofit2:retrofit", version.ref = "retrofit" }
retrofit-adapter-rxjava = { module = "com.squareup.retrofit2:adapter-rxjava2", version.ref = "retrofit" }

在代码里这样写即可:

implementation(commonLibs.retrofit)
implementation(commonLibs.retrofit.adapter.rxjava)
3.4 task

类似 preBuild 这种任务的名字,中间都会包含 build variant 的名字,但是在任务执行的时候是拿不到当前的 build variant 的,所以这里运用迪卡尔积得出所有 flavor 和 build type 的组合得出所有可能的任务名,然后进行配置。

/**
 * 迪卡尔积,用于字符串
 *
 * @param arrays 参数列表
 * @return 笛卡尔积
 * arra1: ["a", "b"]
 * arr2: ["c", "d"]
 * result: ["ac", "ad", "bc", "bd"]
 */
fun cartesianProductString(array1: Array<String>, vararg arrays: Array<String>): List<String> {
    return arrays.fold(array1.toList()) { acc, array ->
        acc.flatMap { list ->
            array.map { element -> list + element }
        }
    }
}

afterEvaluate {
    val flavors = arrayOf("Basic", "Coexist")
    val buildTypes = arrayOf("Debug", "Release")
    cartesianProductString(flavors, buildTypes).forEach {
        tasks.getByName("pre${it}Build") {
            dependsOn(tasks.getByName("xxx"))
        }
    }
}

作者:流沙三七
链接:https://juejin.cn/post/7148437364002521125
来源:稀土掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。

Logo

开放原子开发者工作坊旨在鼓励更多人参与开源活动,与志同道合的开发者们相互交流开发经验、分享开发心得、获取前沿技术趋势。工作坊有多种形式的开发者活动,如meetup、训练营等,主打技术交流,干货满满,真诚地邀请各位开发者共同参与!

更多推荐