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
相关推荐
ace望世界1 小时前
安卓的ViewModel
android
CYRUS_STUDIO4 小时前
一文搞懂 Frida Stalker:对抗 OLLVM 的算法还原利器
android·逆向·llvm
zcychong4 小时前
ArrayMap、SparseArray和HashMap有什么区别?该如何选择?
android·面试
CYRUS_STUDIO4 小时前
Frida Stalker Trace 实战:指令级跟踪与寄存器变化监控全解析
android·逆向
ace望世界9 小时前
android的Parcelable
android
顾林海9 小时前
Android编译插桩之AspectJ:让代码像特工一样悄悄干活
android·面试·性能优化
叽哥10 小时前
Flutter Riverpod上手指南
android·flutter·ios
循环不息优化不止10 小时前
安卓开发设计模式全解析
android
诺诺Okami10 小时前
Android Framework-WMS-层级结构树
android