github blog
qq: 2383518170
wx: lzyprime

λ:

仓库地址: https://github.com/lzyprime/android_demos

本来想把compose版本分离成单独分支:dev_compose; 但是后来发现与dev分支除了view层不太一样,剩下的全是同样代码;甚至view层一些compose组件也全是一样的。

model层里,对数据组织和封装在频繁的改动,想找到更合理易用的方式,比如对DataStore的提供和使用方式,已经调整过好几版,目前的仍不是满意版本。

如果两个分支,这部分代码同步就很烦人。git sub module, git rebase, 手动复制。哪一个都不方便。

所以,把view以外公用的部分,抽成单独的gradle module, composeview部分也抽成一个module。然后在gradle脚本里配好依赖关系。

同时,将gradle脚本由 Groovy 迁到 KTS

gradle kts

gradle 官网文档

android 官网迁移文档

迁移完发现android官网文档居然也提了这事。

好处

  • 相比groovy, 对kotlin更熟悉。脚本易读性提高,对脚本中每一步在执行什么,什么意思更容易掌握,点开看源码和注释。
  • 更规范,去糖。groovy为了脚本编写便捷,提供了一堆简便写法,而很多其实是靠字符串解析,看是否符合规则,然后去调用真正的接口。
  • 接口废弃等提醒。gradle即将废弃接口,接口警告信息等等,都会向kotlin代码一样,直接突出显示。
  • KTS版本去学习gradle的用法。之后就算是groovy版本的,也能看个大概,看着官网文档和基础语法也能写的差不多。可能简便写法不怎么会,但是中规中矩的脚本能跑应该没问题。

坏处

  • 相比groovy, 肯定还是简陋,不完善。包括文档里,常常会有只支持groovy的提示。

迁移过程

gradle 迁移文档

gradle脚本文件名加上.kts后缀(如build.gradle -> build.gradle.kts), 然后sync一下, 解决所有报错。每次最好只改一个文件,否则报错难修。

  • 字符串必须全是双引号
  • 函数调用加括号。如classpath, implementation等等后面空格加字符串的,一般是函数调用。改成classpath(xxx)样式
  • 属性值,如 minSdk, targetSdk, versionCode等等被做成了属性。同时如果属性为bool类型,名字会变成isXXX的形式。
  • tasks, ext, extra, buildSrc

tasks

每个task, 包含name:Stringargs:MapconfigureClosure: Function. groovy提供了一堆简便写法,但最终肯定归到这三部分。以task clean为例。

// groovy
task clean(type: Delete) {
    delete rootProject.buildDir
}

如果点进去,会发现批到的是task(name:String),后边部分都会当字符串处理。这就是groovy提供便捷写法的方式之一,字符串解析。最后相当于:

// 伪代码
task(
    args: {"type": Delete::class}, 
    name: "clean", 
    configureClosure: { // Delete
        delete(rootProject.buildDir)
    },
)

的确简便写法够简洁形象,就像声明一个函数。可是不看源码之类的,谁知道是什么。

kotlin也提供了一堆简便写法,以incline function的形式,可以一层层点到最后。

gradle 任务文档

ext问题

KTS 也有 ext函数,但如果像之前在buildScript块里写,就会报错。点进去就知道原因:

val org.gradle.api.Project.`ext`: org.gradle.api.plugins.ExtraPropertiesExtension get() =
    (this as org.gradle.api.plugins.ExtensionAware).extensions.getByName("ext") as org.gradle.api.plugins.ExtraPropertiesExtension

fun org.gradle.api.Project.`ext`(configure: Action<org.gradle.api.plugins.ExtraPropertiesExtension>): Unit =
    (this as org.gradle.api.plugins.ExtensionAware).extensions.configure("ext", configure)

也就是尝试把当前对象转为ExtensionAware。在groovy中,buildScriptProject的方法,Project实现了ExtensionAware接口。在KTS里,buildScript 来自KotlinBuildScript抽象类, 是个ProjectDelegate,用委托的方式访问Project, 往上找基类也的确是Project

但是buildScript函数接收的是操作ScriptHandlerScope类型。

@Suppress("unused")
open fun buildscript(@Suppress("unused_parameter") block: ScriptHandlerScope.() -> Unit): Unit =
        internalError()

// use:
buildscript { // this: ScriptHandlerScope
    ...
}

也就是说,代码块里的 this 是个 ScriptHandlerScope, 并没有实现ExtensionAware。 所以ScriptHandlerScope as ExtensionAware失败了。

这也是为什么ext在顶级块里写或者在allprojects块里可以正常工作:

buildScript {...}

// this: ProjectDelegate
ext {
    set("key", "value")
}

allprojects { // this: Project
    ext {
        set("k", "v")
    }
}

tasks.register<Delete>("clean") {
    rootProject.ext["key1"] // 指定Project
    delete(rootProject.buildDir)
}

但这只是定义的时候,使用的话,同样因为这种限制,要看清楚作用域,是否能转为ExtensionAware,还要搞清楚是谁的。

同时受kotlin静态语言的限制,想直接Project.ext.key1, 甚至Project.key1使用,是不可能的。就得Project.ext["key1"]

tasks.register<Delete>("clean") {
    val key1 = rootProject.ext["key1"] // 指定Project
    delete(rootProject.buildDir)
}

但是在buildScript里这么写又过不去。此时通过ExtensionAware.extentions.getByName("ext")还拿不到。其实在groovy中也是点不进去的,可以看看groovy怎么处理的,怎么达到动态语言的效果。

ext -> extra

所以这东西基本就废了。然后提供了extra

buildscript {
    val gradleVersion by extra("7.0.1")
    val kotlinVersion by extra{ "1.5.21" }

    extra["activityVersion"] = "1.3.1"
    extra["lifecycleVersion"] = "2.3.1"
}

// module project
val kotlinVersion: String by rootProject.extra

val activityVersion: String by rootProject.extra
val lifecycleVersion: String by rootProject.extra

如果通过委托属性的方式获取值。需要显式声明类型。源码:

val ExtensionAware.extra: ExtraPropertiesExtension
    get() = extensions.extraProperties

也就是说,其实和ext拿到的是一样的,Project.ext其实就是在把ExtensionAware.extensions.extraProperties抛出去。

所以基础的set get等仍然好使。额外添加了一堆委托属性和函数,方便创建获取变量。

val kkk by extra(vvv):

// val kkk by extra(vvv)
operator fun <T> ExtraPropertiesExtension.invoke(initialValue: T): InitialValueExtraPropertyDelegateProvider<T> =
    InitialValueExtraPropertyDelegateProvider.of(this, initialValue)
    // InitialValueExtraPropertyDelegateProvider(extra, vvv)


class InitialValueExtraPropertyDelegateProvider<T>
private constructor(
    private val extra: ExtraPropertiesExtension,
    private val initialValue: T
) {
    companion object {
        fun <T> of(extra: ExtraPropertiesExtension, initialValue: T) =
            InitialValueExtraPropertyDelegateProvider(extra, initialValue)
    }

    operator fun provideDelegate(thisRef: Any?, property: kotlin.reflect.KProperty<*>): InitialValueExtraPropertyDelegate<T> {
        // 插入, 变量名(kkk) 作为key
        extra.set(property.name, initialValue)
        return InitialValueExtraPropertyDelegate.of(extra)
        // InitialValueExtraPropertyDelegate(extra)
    }
}

class InitialValueExtraPropertyDelegate<T>
private constructor(
    private val extra: ExtraPropertiesExtension
) {
    companion object {
        fun <T> of(extra: ExtraPropertiesExtension) =
            InitialValueExtraPropertyDelegate<T>(extra)
    }

    // 赋值操作。 kkk = nvvv -> extra.set(kkk, nvvv)
    operator fun setValue(receiver: Any?, property: kotlin.reflect.KProperty<*>, value: T) =
        extra.set(property.name, value)

    // 取值操作。val nk = kkk -> val nk = extra.get(kkk)
    @Suppress("unchecked_cast")
    operator fun getValue(receiver: Any?, property: kotlin.reflect.KProperty<*>): T =
        uncheckedCast(extra.get(property.name))
}

中规中矩的委托。val kkk: T by extra也是一样:

operator fun ExtraPropertiesExtension.provideDelegate(receiver: Any?, property: KProperty<*>): MutablePropertyDelegate =
    if (property.returnType.isMarkedNullable) NullableExtraPropertyDelegate(this, property.name)
    else NonNullExtraPropertyDelegate(this, property.name)

private
class NonNullExtraPropertyDelegate(
    private val extra: ExtraPropertiesExtension,
    private val name: String
) : MutablePropertyDelegate {

    override fun <T> getValue(receiver: Any?, property: KProperty<*>): T =
        if (!extra.has(name)) cannotGetExtraProperty("does not exist")
        else uncheckedCast(extra.get(name) ?: cannotGetExtraProperty("is null"))

    override fun <T> setValue(receiver: Any?, property: KProperty<*>, value: T) =
        extra.set(property.name, value)

    private
    fun cannotGetExtraProperty(reason: String): Nothing =
        throw InvalidUserCodeException("Cannot get non-null extra property '$name' as it $reason")
}

getValue, setValue是根据变量类型做类型转换。所以要写类型,还要写对。

buildSrc

kotlin dsl plugin 文档

另外完成共享的方式。在rootProject目录下创建buildSrc文件夹,并创建build.gradle.kts

/
|-buildSrc
  |- src/main/kotlin/xxx.kt
  |- build.gradle.kts
//buildSrc/build.gradle.kts
plugins {
    `kotlin-dsl`
}

repositories {
    mavenCentral()
}

src/main/kotlin下的内容在工程内共享。所以可以把变量定义在这:

// src/main/kotlin/versions.kt
const val kotlinVersion = "1.5.30"
...

其他地方可以直接用。

好处是往gradle添加附加功能更方便,易于管理。

弊端就是变量如果放在这,IDE可视化的Project Structure识别失败,就会一直提示有内容可以更新。

module 管理

没什么可讲的。new 一个 module。 根据需要选择类型。然后就是build.gradle.kts处理好依赖和构建。settings.gradle.ktsinclude

当 A_module 依赖 B_module:

// A_module build.gradle.kts
dependencies {
    implementation(project(":B_module"))

更多具体操作可以看文档。转成KTS不就是为了文档读着更容易。