kotlin的委托

kotlin的委托分为委托类和委托属性

如果你是Android工程师,就会发现JetpackCompose当中大量使用了Kotlin委托 特性。可以说,如果你不理解委托,你就无法真正理解JetpackCompose。

一、委托类

先从委托类开始,它的使用场景非常简单易懂:它常常用于实现类的"委托模式"。 来看个简单例子:

kotlin 复制代码
interface DB {
    fun save()
}

class SqlDB() : DB {
    override fun save() {
        println("save to sql")
    }
}

class GreenDaoDB() : DB {
    override fun save() {
        println("save to GreenDao")
    }
}

//                参数 通过 by 将接口实现委托给 db
//                 ↓            ↓
class UniversalDB(db: DB) : DB by db

fun main() {
    UniversalDB(SqlDB()).save()
    UniversalDB (GreenDaoDB()).save()
}
//输出
//save to sql
//save to GreenDao

上面定义了一个DB接口,它的saveO方法用于数据库存储,SqIDB和GreenDaoDB都实现了这个接口。

接着,UniversalDB也实现了这个接口,同时通过by这个关键字,将接口的实现委托给了它的参数db。

这种委托模式在我们的实际编程中十分常见,UniversalDB相当于一个壳,它虽然实现了DB 这个接口,但并不关心它怎么实现。具体是用SQL还是GreenDao,传不同的委托对象进 去,它就会有不同的行为。

另外,以上委托类的写法,等价于以下Java代码,我们可以再进一步来看下:

java 复制代码
public class UniversalDB implements DB {
    DB db;
    public UniversalDB(DB db) {
        this.db = db;
    }


    @Override
    public void save() {
        // 将save委托给db.save()
        db.save();
    }
}

上面代码中save() 将执行流程委托给了传入的db对象。

所以 Kotlin的委托类提供了语法层面的委托模式。 通过这个by关键字,就可以自动将接口里的方法委托给一个对象,从 而可以省略很多接口方法适配的模板代码。

此时可能会有这样的疑问,委托这有什么用?

既然都是调用辅助对象的方法来实现,我还不如直接使用辅助对象。 这种模式的意义在于,可以选择性地重写一部分方法的实现,或是在委托的基础上加入新的方法,从而创建功能更加、丰富的"新"数据结构。

重写委托接口的实现

再看个例子

kotlin 复制代码
interface Base {
    fun printMessage()
    fun printMessageLine()
}

class BaseImpl(val x: Int) : Base {
    override fun printMessage() {
        println(x)
    }
    override fun printMessageLine() {
        println(x)
    }
}

class Derived(b: Base) : Base by b {
    override fun printMessage() {
        println("abc")
    }
}

fun main(){
    val base = BaseImpl(10)
    Derived(base).printMessage()
    Derived(base).printMessageLine()
}

注意这里Derived(base).printMessage() 会打印abc而不是10

再看一个例子:

kotlin 复制代码
interface Base {
    val message: String
    fun print()
}

class BaseImpl(val x: Int) : Base {
    override val message = "BaseImpl: x= $x"
    override fun print() {
        println("BaseImpl print:$message")
    }
}

class Derived(b: Base) : Base by b {
    override val message = "Message of Derived"
//    override fun print() {
//        println("Derived print:$message")
//    }
}

fun main(){
    val b = BaseImpl(10)
    val derived = Derived(b)
    derived.print()
    println(derived.message)
}

注意这里如果Derived不重写print方法,那么derived.print()实际打印的是: "BaseImpl print:BaseImpl: x= 10"

二、委托属性

委托属性的核心思想是:将一个属性(field)的读写操作委托给另一个类去完成。

看个例子:

kotlin 复制代码
class Example {
    var p: String by Delegate()
}

它代表将 p 属性的 get()set() 方法实现委托给了 Delegate 类。

当访问 p 属性时,会自动调用 Delegate 中的 getValue() 方法;

当给 p 属性赋值时,会自动调用 Delegate 中的 setValue() 方法。

注意这里如果是val p,那么委托的是p的get方法。

看一下Delegate类是什么

kotlin 复制代码
import kotlin.reflect.KProperty

class Delegate {
    operator fun getValue(thisRef: Any?, property: KProperty<*>): String {
        return "$thisRef, thank you for delegating '${property.name}' to me!"
    }

    operator fun setValue(thisRef: Any?, property: KProperty<*>, value: String) {
        println("$value has been assigned to '${property.name}' in $thisRef.")
    }
}

这是标准的代码实现模板:

  • getValue() 方法

    • 一个参数用于声明该Delegate类的委托功能可以在什么类中使用。例如,在 Example 的实例中访问 p 属性时,这个参数就是 Example 实例。
    • 第二个参数数KProperty<>是Kotlin 中的一个属性操作类,可用于获取各种属性相关的值.用于获取属性的元信息,比如属性名(prop.name)。另外,《>这种泛型的写法表示你不知道或者不关心泛型的具体类型,只是为了通过语法编译而已,有点类似于Java中<?>的写法。
    • 方法的返回值类型需要和委托属性的类型兼容。不是相同,因为返回值类型可以转为委托属性的类型即可。
  • setValue() 方法

    • 前两个参数和 getValue 相同
    • 第三个参数代表要赋给委托属性的新值。注意:这个参数的类型要和 getValue 方法返回值的类型兼容

不过当委托的属性使用 val 声明时,可以不用实现 setValue 方法,

下面具体看下用法

当你从 p 读取委托给 Delegate 实例时,将调用 DelegategetValue() 函数。它的第一个参数是你从中读取 p 的对象,第二个参数包含 p 本身的描述(例如,可以取其名称)

kotlin 复制代码
val e = Example()
println(e.p)

这将打印

css 复制代码
Example@33a17727, thank you for delegating 'p' to me!

同样,当你赋值给 p 时,会调用 setValue() 函数。前两个参数相同,第三个参数保存要分配的值

kotlin 复制代码
e.p = "NEW"

将打印

css 复制代码
NEW has been assigned to 'p' in Example@33a17727.

三、标准委托

Kotlin"委托类"委托的是接口方法。

而"委托属性"委托的,则是属性的getter、setter。

我们知道val定义的属性,它只有get()方法;

而var定义的属性,既有get()方法,也有set()方法。

那么,属性的getter、setter委托出去以后,能有什么用呢?

我们可以从Kotlin官方提供的标准委托那里找到答案。

标准委托

Kotlin提供了好几种标准委托,其中包括:

  1. 两个属性之间的直接委托
  2. by lazy懒加载委托
  3. Delegates.observable观察者委托
  4. by map映射委托。

前面两个的使用频率比较高,后面两个频率比较低。

将属性A委托给属性B

从Kotlin1.4开始,我们可以直接在语法层面将"属性A"委托给"属性B",就像下面这 样:

kotlin 复制代码
class Item {
    var count: Int =0
    var total: Int by ::count
}

这里定义了两个变量,count和total, 因为把total这个属性的getter和setter都委托给了count,所以其中total的值与count完全一致,

注意:

by 代表total属性的getter、setter会被委托出去; ::count,代表total被委托给了count。这里的"::count"是属性的引用

total和count两者之间的委托关系一旦建立,就代表了它们两者的getter和setter会完全 绑定在一起,如果要用代码来解释它们背后的逻辑,它们之间的关系会是这样:

kotlin 复制代码
//近似逻辑,实际上,底层会生成一个ItemStotaLS2类型的delegate来实现
class Item {
    var count:Int = 0

    var total: Int
        get() = count
        set(value: Int) {
            count = value
        }
}

也就是,当total的get()方法被调用时,它会直接返回count的值,也就意味着会调用 count的get()方法;

而当total的set()方法被调用时,它会将value传递给count,也就 意味着会调用count的set()方法。

也许你会好奇:Kotlin1.4提供的这个特性有啥用? 为什么要分别定义count和total? 直接用count不好吗?

这个特性,其实对我们软件版本之间的兼容很有帮助。假设Item是服务端接口的返回数据, 1.0版本的时候,我们的Item当中只count这一个变量:

kotlin 复制代码
class Item {
    var count:Int = 0
}

而到了2.0版本的时候,我们需要将count修改成total。

这时候问题就出现了,如果我们直接将count修改成total,我们的老用户就无法正常使用了。

但如果我们借助委托,就可以很方便地实现这种兼容。 我们可以定义一个新的变量total,然后将其委托给count。

这样的话,2.0的用户访问total,而1.0的用户访问原来的count,由于它们是委托关系,也不必 担心数值不一致的问题。

懒加载委托

懒加载,就是对于一些需要消耗计算机资源的操作,希望它在被访问的时候才 去触发,从而避免不必要的资源开销。其实,这也是软件设计里十分常见的模式。

看个例子:

kotlin 复制代码
val data: String by lazy{
    request()
}

fun request(): String {
    println("执行网络请求")
    return "返回数据"
}

fun main() {
    println("开始")
    println(data)
    println(data)
}
//开始
//执行网络请求
//返回数据
//返回数据

通过"by lazy{}",就可以实现属性的懒加载了。

这样,上面的执行结果我们会发现: main函数的第一行代码,由于没有用到data,所以request函数也不会被调用。

第二行代码,我们要用到data的时候,request才会被触发执行。

到了第三行代码,由于前面我们已经知道了data的值,因此也不必重复计算,直接返回结果即可。 所以日志中看到"执行网络请求"只打印了1次

这里lazy其实是个高阶函数,并不是什么关键字

kotlin源码 复制代码
public actual fun <T> lazy(initializer: () -> T): Lazy<T> = SynchronizedLazyImpl(initializer)

public actual fun <T> lazy(mode: LazyThreadSafetyMode, initializer: () -> T): Lazy<T> =
    when (mode) {
        LazyThreadSafetyMode.SYNCHRONIZED -> SynchronizedLazyImpl(initializer)
        LazyThreadSafetyMode.PUBLICATION -> SafePublicationLazyImpl(initializer)
        LazyThreadSafetyMode.NONE -> UnsafeLazyImpl(initializer)
    }

lazy() 是一个接受 lambda 并返回 Lazy<T> 实例的函数,它可以作为实现惰性属性的委托。

第一次调用 get() 会执行传递给 lazy() 的 lambda 并记住结果。后续对 get() 的调用只是返回记住的结果。

可以看到,lazy函数还可以接收一个LazyThreadSafetyMode类型的参数,如果我们不传这个参数,它就会直接使用SynchronizedLazylmpl的方式。

而从LazyThreadSafetyMode.SYNCHRONIZED -> SynchronizedLazyImpl(initializer) 这句可以看出来,它是为了多线程同步的。而剩下的SafePublicationLazylmpl、UnsafeLazylmpl,则不是多线程安全的。

默认情况下不传LazyThreadSafetyMode,延迟属性的计算是同步的 :该值仅在一个线程中计算,但所有线程都将看到相同的值。

如果不需要多线程同步可以将 LazyThreadSafetyMode.PUBLICATION 作为参数传递给 lazy()

如果确定初始化将始终与使用该属性的线程在同一线程中进行,则可以使用 LazyThreadSafetyMode.NONE。它不会产生任何线程安全保证和相关开销。

实现简化的 lazy 函数

了解了委托属性后,可以实现一个自己的 lazy(懒加载)函数。它会将属性的初始化代码延迟到它第一次被访问时,才被执行。

下面来实现一个自己的懒加载函数 later

首先,实现符合委托规范的 Later 类。代码如下

kotlin 复制代码
class Later<T>(private val block: () -> T) {
    private var value: Any? = null 

    operator fun getValue(thisRef: Any?, prop: KProperty<*>): T {
        if (value == null) {
            println("inside later block (initializing...)")
            value = block()
        }
        @Suppress("UNCHECKED_CAST")
        return value as T
    }
}

然后,创建一个顶层 later 函数,用于返回 Later 类的实例,代码如下:

kotlin 复制代码
fun <T> later(block: () -> T) = Later(block)

测试一下:

kotlin 复制代码
fun main() {
    val p by later {
        println("inside later block")
        "this a later string"
    }
    println("before access p")
    println("p is $p")
    println("after access p")
}

可以看到 Lambda 表达式中的初始化代码,只有在第一次访问 p 属性时才会执行。这就是实现了懒加载的效果。

最后注意:这只是大致实现了 lazy 函数的功能。在真实项目中,还是要使用标准库中的 lazy 函数。因为它解决了线程安全的问题,防止在多线程的环境下,导致初始化操作被重复执行;解决了空值处理的问题,如果 block 块执行的结果是空(value = null),那么下次访问委托的属性而调用 getValue 方法时,还是会进入 value == null 的判断,导致重复执行 block 块中的代码。

自定义委托

kotlin 复制代码
import kotlin.reflect.KProperty

class StringDelegate(private var s: String = "hello") {
    operator fun getValue(thisRef: Owner, property: KProperty<*>): String {
        return s
    }

    operator fun setValue(thisRef: Owner, property: KProperty<*>, value: String) {
        s = value
    }
}

class Owner {
    var text: String by StringDelegate()
}

首先,对于var修饰的属性,我们必须要有getValue、 setValue这两个方法,同时,这两个方法必须有operator关键字修饰。

其次,我们的text属性是处于Owner这个类当中的,因此 getValue、setValue这两个方法中的thisRef的类型,必须要是Owner,或者是Owner的 父类。也就是说,我们将thisRef的类型改为 Any 也是可以的。

当我们不确定委托属性会处于哪个类的时候,就可以将thisRef的类型定义为 "Any?" 。

最后,由于我们的text属性是String类型的,为了实现对它的 委托,getValue的返回值类型,以及setValue的参数类型,都必须是String类型或者是它 的父类。

而如果你觉得这样的写法实在很繁琐,也可以借助Kotlin提供的ReadWriteProperty、 ReadOnlyProperty这两个接口,来自定义委托。

如果我们需要为val属性定义委托,我们就去实现ReadOnlyProperty这个接口;如果我们 需要为var属性定义委托,我们就去实现ReadWriteProperty这个接口。这样做的好处是, 通过实现接口的方式,Intellij可以帮我们自动生成override的getValue、setValue方法。

以前面的代码为例,我们的 StringDelegate,也可以通过实现ReadWriteProperty接口来编 写:

kotlin 复制代码
class StringDelegate(private var s: String = "hello"): ReadWriteProperty<Owner, String> {
    override operator fun getValue(thisRef: Owner, property: KProperty<*>): String {
        return s
    }

    override operator fun setValue(thisRef: Owner, property: KProperty<*>, value: String) {
        s = value
    }
}

Delegates.observable观察者委托

Delegates.observable() 接受两个参数:初始值和修改处理程序

每次分配给属性时(执行赋值后 )都会调用处理程序。 它有三个参数:要赋值的属性、旧值和新值:

kotlin 复制代码
import kotlin.properties.Delegates

class User {
    var name: String by Delegates.observable("<no name>") {
        prop, old, new ->
        println("$old -> $new")
    }
}

fun main() {
    val user = User()
    user.name = "first"
    user.name = "second"
}
// 执行结果:
//<no name> -> first
//first -> second

vetoable 是 Kotlin 标准库给 可变属性(var) 提供的一个 "拦截器" ------

真正赋值之前 先跑一段你自己的逻辑,

如果你返回 false ,这次赋值就被 否决(veto) ,属性值保持原样;

返回 true 才正式生效。

一句话: "想改可以,先过我这关。"

函数签名

kotlin 复制代码
public inline fun <T> vetoable(
    initialValue: T,                       // 初始值
    crossinline onChange: (property: KProperty<*>, oldValue: T, newValue: T) -> Boolean
): ReadWriteProperty<Any?, T>

onChange 返回 Boolean

  • true → 接受新值
  • false → 拒绝新值,属性不变
kotlin 复制代码
var percentage: Int by Delegates.vetoable(0) { _, old, new ->
    // 只允许 0..100 之间的数
    new in 0..100
}

fun main() {
    percentage = 50   // 成功
    println(percentage) // 50

    percentage = 200  // 被否决
    println(percentage) // 仍是 50
}

典型使用场景

  1. 范围校验 / 业务规则
kotlin 复制代码
var price: BigDecimal by Delegates.vetoable(BigDecimal.ZERO) { _, _, new ->
    new >= BigDecimal.ZERO          // 拒绝负价
}
  1. 防抖动(拒绝相同值)
kotlin 复制代码
var selectedId: Long by Delegates.vetoable(-1) { _, old, new ->
    new != old      // 相同 ID 直接无视
}
  1. 状态机驱动:拒绝非法转移
sql 复制代码
enum class State { IDLE, RUNNING, PAUSED, STOPPED }

var state: State by Delegates.vetoable(State.IDLE) { _, old, new ->
    // 只允许合法转移
    when {
        old == State.IDLE   && new == State.RUNNING -> true
        old == State.RUNNING && new == State.PAUSED -> true
        old == State.PAUSED  && new == State.RUNNING -> true
        old == State.RUNNING && new == State.STOPPED -> true
        else -> false
    }
}
  1. Android 实战:View 宽高只能增大
javascript 复制代码
var w: Int by Delegates.vetoable(0) { _, old, new -> new >= old }
var h: Int by Delegates.vetoable(0) { _, old, new -> new >= old }

by map映射委托

一种常见的用例是将属性值存储在map中。这经常出现在解析 JSON 或执行其他动态任务等应用程序中。在这种情况下,可以使用映射实例本身作为委托属性的委托。

kotlin 复制代码
class User(val map: Map<String, Any?>) {
    val name: String by map
    val age: Int     by map
}

在此示例中,构造函数采用一个map:

sql 复制代码
val user = User(mapOf(
    "name" to "John Doe",
    "age"  to 25
))

委托属性通过字符串键从此映射中获取值,这些键与属性名称相关联:

scss 复制代码
println(user.name) // Prints "John Doe"
println(user.age)  // Prints 25

如果使用 MutableMap 而不是只读 Map,这也适用于 var 的属性

kotlin 复制代码
class MutableUser(val map: MutableMap<String, Any?>) {
    var name: String by map
    var age: Int     by map
}

val user = MutableUser(mutableMapOf(
    "name" to "John Doe",
    "age"  to 25
))

fun main() {
    println(user.name)
    println(user.age)
}

本地委托属性

可以将局部变量声明为委托属性。例如,您可以使局部变量惰性:

kotlin 复制代码
fun example(computeFoo: () -> Foo) {
    val memoizedFoo by lazy(computeFoo)

    if (someCondition && memoizedFoo.isValid()) {
        memoizedFoo.doSomething()
    }
}

memoizedFoo 变量将仅在首次访问时计算。如果 someCondition 失败,则根本不会计算变量。

委托属性的转换规则

在后台,Kotlin 编译器会为某些类型的委托属性生成辅助属性,然后委托给它们。 出于优化目的,编译器在某些情况下不会生成辅助属性。

例如,对于属性prop ,它生成隐藏属性 prop$delegate,而访问器的代码只是委托给这个附加属性:

kotlin 复制代码
class C {
    var prop: Type by MyDelegate()
}

// this code is generated by the compiler instead:
class C {
    private val prop$delegate = MyDelegate()
    var prop: Type
        get() = prop$delegate.getValue(this, this::prop)
    set(value: Type) = prop$delegate.setValue(this, this::prop, value)
}

otlin 编译器在参数中提供了有关 prop 的所有必要信息:第一个参数 this 引用了外部类 C 的实例,this::prop 是描述 prop 本身的 KProperty 类型的反射对象。

委托属性的优化案例

如果委托是以下情况,则将省略 $delegate 字段:

  1. 引用的属性:
csharp 复制代码
class C<Type> {
    private var impl: Type = ...
    var prop: Type by ::impl
}
  1. 命名对象:
kotlin 复制代码
object NamedObject {
    operator fun getValue(thisRef: Any?, property: KProperty<*>): String = ...
}

val s: String by NamedObject
  1. 在同一模块中具有支持字段和默认 getter 的最终 val 属性 优化触发条件(四条必须全部满足) 4. val (不能是 var) 5. final (不能是 open / override) 6. 有幕后字段 (也就是不是 get() = ... 这种纯自定义 getter)

  2. 默认 getter (你没有手写 get() = ...

  3. 代理实例是编译期已知的常量 (最常见的就是同一模块 里的顶层 val impl = ...

kotlin 复制代码
// file: Util.kt
val impl = ReadOnlyProperty<Any?, String> { _, _ -> "hello" }

// file: A.kt
class A {
    val s: String by impl
}

编译器不再生成 $$delegate,而是直接把 getter 内联成:

kotlin 复制代码
public final class A {
    // <== 没有 $$delegate_0 字段
    @NotNull
    public final String getS() {
        return impl.getValue(this, $$s$metadata); // 直接引用顶层单例 impl
    }
}

在 Kotlin 里,val 默认就是 final ,你不需要、也不能再显式写 final 只有当属性是 var 或者显式写成 open val / override val 时,才会失去"final"这一优化前提。

  1. constant expression常量表达式, enum 枚举,this,null。 this的例子:
kotlin 复制代码
class A {
    operator fun getValue(thisRef: Any?, property: KProperty<*>) ...

    val s by this
}

委托给其他属性时的转换规则

委托给另一个属性时,Kotlin 编译器会立即生成对引用属性的访问权限。这意味着编译器不会生成字段 prop$delegate。这种优化有助于节省内存。

以以下代码为例:

csharp 复制代码
class C<Type> {
    private var impl: Type = ...
    var prop: Type by ::impl
}

prop 变量的属性访问器直接调用 impl 变量,跳过委托属性的 getValuesetValue 运算符,因此不需要 KProperty 引用对象。

对于上面的代码,编译器会生成以下代码:

kotlin 复制代码
class C<Type> {
    private var impl: Type = ...

    var prop: Type
        get() = impl
        set(value) {
            impl = value
        }

    fun getProp$delegate(): Type = impl // 这个方法只为反射使用
}

提供委托(provideDelegate)

实际上,要想在属性委托之前再做一些额外的判断工作,我们可以使用provideDelegate来 实现。

看看下面的SmartDelegator你就会明白:

kotlin 复制代码
class SmartDelegator {
    operator fun provideDelegate(
        thisRef: Owner,
        prop: KProperty<*>
    ): ReadWriteProperty<Owner, String> {
        return if (prop.name.contains("log")) {
            return StringDelegate("log")
        } else {
            return StringDelegate("normal")
        }
    }
}

class Owner{
    var normalText: String by SmartDelegator()
    var logText: String by SmartDelegator()
}

fun main() {
    val owner = Owner()
    println(owner.normalText)
    println(owner.logText)
}

为了在委托属性的同时进行一些额外的逻辑判断,我们使用创建了一个新的 SmartDelegator,通过它的成员方法provideDelegate 嵌套了一层,在这个方法当中,我们 进行了一些逻辑判断,然后再把属性委托给StringDelegate。

这样,通过provideDelegate ,不仅可以嵌套Delegator,还可以根据 不同的逻辑派发不同的Delegator。

实战与思考

案例1:属性可见性封装

在软件设计当中,我们会遇到这样的需求:对于某个成员变量data,我们希望类的外部可以 访问它的值,但不允许类的外部修改它的值。因此我们经常会写出类似这样的代码:

kotlin 复制代码
class Model {
    var data: String = ""
        // 我们将data属性的set方法声明为private的,
        // 这时候,data属性的set方法只能从类的内部访问,
        // 这就意味着类的外部无法修改data的值了,
        // 但类的外部仍然可以访问data的值
        private set

    private fun load(){
        // 网络请求
        data = "请求结果"
    }
}

这样的代码模式很常见,我们在Java/C当中也经常使用,不过当我们的data类型从String 变成集合以后,问题就不一样了。

kotlin 复制代码
class Model {
    val data: MutableList<String> = mutableListOf()

    private fun load(){
        // 网络请求
        data.add("hello")
    }
}

fun main() {
    val model = Model()
    // 类的外部仍然可以修改data
    model.data.add("World")
}

对于集合而言,即使我们将其定义为只读变量val,类的外部一旦获取到data的实例,它仍 然可以调用集合的addO方法修改它的值。这个问题在Java当中几乎没有优雅的解法。只要 你暴露了集合的实例给外部,外部就可以随意修改集合的值。这往往也是Bug的来源,这样 的Bug 还非常难排查。

而在这个场景下,我们前面学习的"两个属性之间的委托"这个语法,就可以派上用场了。

kotlin 复制代码
class Model {
    val data: List<String> by:: _data
    private val _data: MutableList<String> = mutableListOf()
    fun load() {
        _data.add("Hello")
    }
}

fun main() {
    val model = Model()
    // 编译报错
    model.data.add("World")
}

在上面的代码中,我们定义了两个变量,一个变量是公开的"data",它的类型是List,这是 Kotlin当中不可修改的List,它是没有add、remove等方法的。

接着,我们通过委托语法,将data的getter委托给了_data这个属性。而_data这个属性 的类型是MutableList,这是Kotlin当中的可变集合,它是有add、remove方法的。由于 它是private修饰的,类的外部无法直接访问,通过这种方式,我们就成功地将修改权保留在 了类的内部,而类的外部访问是不可变的List,因此类的外部只能访问数据。

案例2:数据与View的绑定

在Android当中,如果我们要对"数据"与"View"进行绑定,我们可以用DataBinding, 不过DataBinding太重了,也会影响编译速度。其实,除了DataBinding以外,我们还可以 借助Kotlin的自定义委托属性来实现类似的功能。这种方式不一定完美,但也是一个有趣的 思路。

这里我们以TextView为例:

kotlin 复制代码
operator fun TextView.provideDelegate(value: Any?, property: KProperty<*>) = object : ReadWriteProperty<Any?, String?> {
        override fun getValue(thisRef: Any?, property: KProperty<*>): String? = text
        override fun setValue(thisRef: Any?, property: KProperty<*>, value: String?) {
            text = value
        }
    }

以上的代码,为TextView定义了一个扩展函数TextView.provideDelegate,

而这个扩展函数的返回值类型是ReadWriteProperty。

通过这样的方式,我们的TextView就相当于支持了String属性的委托了。

它的使用方式也很简单:

arduino 复制代码
val textView = findViewById<textView>(R.id.textView)
    
// ①
var message: String? by textView
    
// ②
textView.text = "Hello"
println(message)

// ③
message = "World"
println(textView.text)
   
结果:
Hello
World

在注释①处的代码,我们通过委托的方式,将message委托给了textView。这意味着,message的getter和setter都将与TextView关联到一起。

在注释②处,我们修改了textView的text属性,由于我们的message也委托给了textView,因此这时候,println(message)的结果也会变成"Hello"。

在注释③处,我们改为修改message的值,由于message的setter也委托给了textView,因此这时候,println(textView.text)的结果会跟着变成"World"。

案例3:ViewModel委托

在Android当中 ViewModel 应该可以算是Jetpack 中最重要的组件之一了

我们会经常用到ViewModel来存储界面数据。同时,我们不会直接创建ViewModel的实例,而对应的,我们会使用委托的方式来实现。

csharp 复制代码
// MainActivity.kt
    
private val mainViewModel: MainViewModel by viewModels()

这一行代码虽然看起来很简单,但它背后隐藏了ViewModel复杂的实现原理。

我们先来看看viewModels()是如何实现的:

kotlin 复制代码
public inline fun <reified VM : ViewModel> ComponentActivity.viewModels(
        noinline factoryProducer: (() -> Factory)? = null
    ): Lazy<VM> {
        val factoryPromise = factoryProducer ?: {
            defaultViewModelProviderFactory
        }
    
        return ViewModelLazy(VM::class, { viewModelStore }, factoryPromise)
    }
    
    public interface Lazy<out T> {
    
        public val value: T
    
        public fun isInitialized(): Boolean
    }

原来,viewModels()是Activity的一个扩展函数

也是因为这个原因,我们才可以直接在Activity当中直接调用viewModels()这个方法。

另外,viewModels()这个方法的返回值类型是Lazy,那么,它是如何实现委托功能的呢?

Lazy类在外部还定义了一个扩展函数getValue(),这样,只读属性的委托就实现了。

kotlin 复制代码
public inline operator fun <T> Lazy<T>.getValue(thisRef: Any?, property: KProperty<*>): T = value
相关推荐
zzhongcy1 小时前
复合索引 (item1, item2, item3 ) > (?, ?, ?) 不起作用,EXPLAIN 后type=ALL(全表扫描)
android·数据库
冬奇Lab2 小时前
稳定性性能系列之十三——CPU与I/O性能优化:Simpleperf与存储优化实战
android·性能优化
像风一样自由3 小时前
android native 中的函数动态注册方式总结
android·java·服务器·安卓逆向分析·native函数动态注册·.so文件分析
nono牛3 小时前
Makefile中打印变量
android
没有了遇见4 小时前
Android 关于RecycleView和ViewPager2去除边缘反馈
android
城东米粉儿4 小时前
android gzip数据压缩 笔记
android
城东米粉儿4 小时前
android 流量优化笔记
android
似霰5 小时前
HIDL Hal 开发笔记10----添加硬件访问服务(Java 层调用 HIDL)
android·framework·hal
佛系打工仔6 小时前
绘制K线第三章:拖拽功能实现
android·前端·ios
我命由我123456 小时前
Android 项目路径包含非 ASCII 字符问题:Your project path contains non-ASCII characters
android·java·java-ee·android studio·android jetpack·android-studio·android runtime