Kotlin托付的常见运用场景

前语

在规划形式中,托付形式(Delegate Pattern)与署理形式都是咱们常用的规划形式(Proxy Pattern),两者十分的相似,又有细小的区别。

托付形式中,托付方针和被托付方针都是同一类型的方针,托付方针将使命托付给被托付方针来结束。托付形式能够用于结束事件监听器、回调函数等功用。

署理形式中,署理方针与被署理方针是两种不同的方针,署理方针代表被署理方针的功用,署理方针能够控制客户对被署理方针的拜访。署理形式能够用于结束远程署理、虚拟署理、安全署理等功用。

以类的托付与署理来举例,托付方针和被托付方针都结束了同一个接口或承继了同一个类,托付方针将使命托付给被托付方针来结束。署理形式中,署理方针与被署理方针结束了同一个接口或承继了同一个类,署理方针代表被署理方针,客户端经过署理方针来拜访被署理方针。

两者的区别:

他们虽然都有同一个接口,主要区别在于托付形式中托付方针和被托付方针是同一类型的方针,而署理形式中署理方针与被署理方针是两种不同的方针。总的来说,托付形式是为了将办法的结束交给其他类去结束,而署理形式则是为了控制方针的拜访,并在拜访前后进行额定的操作。

而咱们常用的托付形式怎样运用?在 Java 语言中需求咱们手动的结束,而在 Kotlin 语言中直接经过关键字 by 就能够结束托付,其结束愈加优雅、简洁了。

咱们在开发一个 Android 运用中,常用到的托付分为:

  1. 接口/类的托付
  2. 特点的托付
  3. 结合lazy的推迟托付
  4. 调查者的托付
  5. Map数据的托付

下面咱们就一同看看不同品种的托付运用以及在 Android 常见的一些场景中的运用。

一、接口/类托付

咱们能够挑选运用接口来结束相似的效果,也能够直接传参,当然接口的办法愈加的灵活,比如咱们这里就以接口比如我界说一个进犯与防护的行为接口:

interface IUserAction {
    fun attack()
    fun defense()
}

界说了用户的行为,有进犯和防护两种操作!接下来咱们就界说一个默许的结束类:

class UserActionImpl : IUserAction {
    override fun attack() {
        YYLogUtils.w("默许操作-开端履行进犯")
    }
    override fun defense() {
        YYLogUtils.w("默许操作-开端履行防护")
    }
}

都是很简略的代码,咱们界说一些默许的操作,假如任意类想拥有进犯和防护的能力就直接结束这个接口,假如想自界说进犯和防护则重写对应的办法即可。

假如运用 Java 的办法结束托付,大致代码如下:

class UserDelegate1(private val action: IUserAction) : IUserAction {
    override fun attack() {
        YYLogUtils.w("UserDelegate1-需求自己结束进犯")
    }
    override fun defense() {
        YYLogUtils.w("UserDelegate1-需求自己结束防护")
    }
}

假如运用 Kotlin 的办法结束则是:

class UserDelegate2(private val action: IUserAction) : IUserAction by action

假如 Kotlin 的结束不想默许的结束也能够重写部分的操作:

class UserDelegate3(private val action: IUserAction) : IUserAction by action {
    override fun attack() {
        YYLogUtils.w("UserDelegate3 - 只重写了进犯")
    }
}

那么运用起来便是这样的:

    val actionImpl = UserActionImpl()
    UserDelegate1(actionImpl).run {
        attack()
        defense()
    }
    UserDelegate2(actionImpl).run {
        attack()
        defense()
    }
    UserDelegate3(actionImpl).run {
        attack()
        defense()
    }

打印日志如下:

Kotlin委托的原理与使用,以及在Android开发中常用的几个场景

其实在 Android 源码中也有不少托付的运用,例如生命周期的 Lifecycle 托付:

Lifecycle 经过托付机制结束其功用。具体来说,组件能够将自己的生命周期状况托付给 LifecycleOwner 方针,LifecycleOwner 方针则担任办理这些组件的生命周期。

例如,在一个 Activity 中,咱们能够经过将 Activity 方针作为 LifecycleOwner 方针,并将该方针传递给需求注册生命周期的组件,然后结束组件的生命周期办理。 页面能够运用 getLifecycle() 办法来获取它所依赖的 LifecycleOwner 方针的 Lifecycle 实例,并在需求时将自身的生命周期状况托付给该 Lifecycle 实例。

经过这种托付机制,Lifecycle 结束了一种方便的办法来办理组件的生命周期,避免了手动办理生命周期带来的麻烦和过错。


class AnimUtil private constructor() : DefaultLifecycleObserver {
    ...
    private fun addLoopLifecycleObserver() {
        mOwner?.lifecycle?.addObserver(this)
    }
    // 退出页面的时分开释资源
    override fun onDestroy(owner: LifecycleOwner) {
        mAnim?.cancel()
        destory()
    }
}

除此之外托付还特别适用于一些可装备的功用,比如 Resutl-Api 的封装,假如当前页面需求开启 startActivityForResult 的功用,就结束这个接口,不需求这个功用就不结束接口,到达可装备的效果。

/**
 * 界说是否需求SAFLauncher
 */
interface ISAFLauncher {
    fun <T : ActivityResultCaller> T.initLauncher()
    fun getLauncher(): GetSAFLauncher?
}

因为代码是固定的结束,方针Activity也不需求从头结束,咱们只需求结束默许的结束即可:

class SAFLauncher : ISAFLauncher {
    private var safLauncher: GetSAFLauncher? = null
    override fun <T : ActivityResultCaller> T.initLauncher() {
        safLauncher = GetSAFLauncher(this)
    }
    override fun getLauncher(): GetSAFLauncher? = safLauncher
}

运用起来咱们直接用默许的结束即可:

class DemoActivity : BaseActivity, ISAFLauncher by SAFLauncher() {
    override fun init() {
        initLauncher()  // 结束了接口还需求初始化Launcher
    }
    fun gotoOtherPage() {
        //运用 Result Launcher 的办法发动,并获取到回来值
        getLauncher()?.launch<DemoCircleActivity> { result ->
            val result = result.data?.getStringExtra("text")
            toast("收到回来的数据:$result")
        }
    }
}

这样是不是就十分简略了呢?具体怎样运用封装 Result Launcher 能够看看我上一年的文章 【传送门】

二、特点托付

除了类与接口方针的托付,咱们还常用于特点的托付。

我知道了!这么弄就行了。

    private val textStr by "123"

哎?怎样报错了?其实不是这么用的。

特点托付和类托付相同,特点的托付其实是对特点的 set/get 办法的托付。

需求咱们把 set/get 办法托付给 setValue/getValue 办法,因而被托付类(真实类)需求供给 setValue/getValue 办法,val特点只需求供给 getValue 办法。

咱们修正代码如下:

    private val textStr by TextDelegate()
    class TextDelegate {
        operator fun getValue(thisRef: Any?, property: KProperty<*>): String {
            return "我是赋值给与的文本"
        }
    }

打印的结果:

Kotlin委托的原理与使用,以及在Android开发中常用的几个场景

而咱们界说一个可读写的特点则能够

  private var textStr by TextDelegate()
    class TextDelegate {
        operator fun getValue(thisRef: Any?, property: KProperty<*>): String {
            return "我是赋值给与的文本"
        }
        operator fun setValue(thisRef: Any?, property: KProperty<*>, value: String) {
            YYLogUtils.w("设置的值为:$value")
        }
    }
    YYLogUtils.w("textStr:$textStr")
    textStr = "abc123"

打印则如下:

Kotlin委托的原理与使用,以及在Android开发中常用的几个场景

为了怕咱们写错,咱们其实能够用接口来约束,只读的和读写的特点,咱们分别能够用 ReadOnlyProperty 与 ReadWriteProperty 来约束:


    class TextDelegate : ReadOnlyProperty<Any, String> {
        override fun getValue(thisRef: Any, property: KProperty<*>): String {
            return "我是赋值给与的文本"
        }
    }
    class TextDelegate : ReadWriteProperty<Any, String> {
        override fun getValue(thisRef: Any, property: KProperty<*>): String {
            return "我是赋值给与的文本"
        }
        override fun setValue(thisRef: Any, property: KProperty<*>, value: String) {
            YYLogUtils.w("设置的值为:$value")
        }
    }

那么结束的办法和上面自己结束的效果是相同的。假如要运用特点托付能够选用这种接口约束的办法结束。

咱们的特点除了托付给类去结束,同时也能托付给其他特点(Kotlin 1.4+)来结束,例如:

    private var textStr by TextDelegate2()
    private var textStr2 by this::textStr

其实是内部托付了方针的 get 和 set 函数。相对托付方针而言功能更好一些。而托付方针去结束,不只增加了一个托付类,并且还还在初始化时就创立了托付类的实例方针,算起来其实功能并不好。

所以特点的托付不要乱用,假如要用,能够挑选托付现成的其他特点来结束,或许运用推迟托付Lazy结束,或许运用更简略的办法结束:

    private val industryName: String
        get() {
            return "abc123"
        }

关于只读的特点,这种办法也是咱们常见的运用办法。

三、推迟托付

假如说运用类来结束托付不那么好的话,其实咱们能够运用推迟托付。推迟关键字 lazy 接收一个 lambda 表达式,最终一行代表回来值给被推脱的特点。

默许的 Lazy 结束:

    val name: String by lazy {
        YYLogUtils.w("第一次调用初始化")
        "abc123"
    }
    YYLogUtils.w(name)
    YYLogUtils.w(name)
    YYLogUtils.w(name)

只要在第一次运用此特点的时分才会初始化,一旦初始化之后就能够直接获取到值。

日志打印:

Kotlin委托的原理与使用,以及在Android开发中常用的几个场景

它的内部其实也是运用的是类的托付结束。

public actual fun <T> lazy(initializer: () -> T): Lazy<T> = SynchronizedLazyImpl(initializer)

最终的结束是由 SynchronizedLazyImpl 类生成并结束的:

private class SynchronizedLazyImpl<out T>(initializer: () -> T, lock: Any? = null) : Lazy<T>, Serializable {
    private var initializer: (() -> T)? = initializer
    @Volatile private var _value: Any? = UNINITIALIZED_VALUE
    // final field is required to enable safe publication of constructed instance
    private val lock = lock ?: this
    override val value: T
        get() {
            val _v1 = _value
            if (_v1 !== UNINITIALIZED_VALUE) {
                @Suppress("UNCHECKED_CAST")
                return _v1 as T
            }
            return synchronized(lock) {
                val _v2 = _value
                if (_v2 !== UNINITIALIZED_VALUE) {
                    @Suppress("UNCHECKED_CAST") (_v2 as T)
                } else {
                    val typedValue = initializer!!()
                    _value = typedValue
                    initializer = null
                    typedValue
                }
            }
        }
    override fun isInitialized(): Boolean = _value !== UNINITIALIZED_VALUE
    override fun toString(): String = if (isInitialized()) value.toString() else "Lazy value not initialized yet."
    private fun writeReplace(): Any = InitializedLazyImpl(value)
}

咱们能够直接看 value 的 get 办法,假如_v1 !== UNINITIALIZED_VALUE 则标明现已初始化过了,就直接回来 value ,不然标明没有初始化过,调用initializer办法,也便是 lazy 的 lambda 表达式回来特点的赋值。

跟咱们自己结束类的托付相似,也是结束了getValue办法。仅仅多了判断是否初始化的一些相关逻辑。

lazy的参数分为三品种型:

  1. SYNCHRONIZED:增加同步锁,使lazy推迟初始化线程安全
  2. PUBLICATION:初始化的lambda表达式,能够在同一时间多次调用,可是只要第一次的回来值作为初始化值
  3. NONE:没有同步锁,非线程安全

默许情况下,关于 lazy 特点的求值是同步锁的(synchronized),是能够确保线程安全的,可是假如不需求线程安全和削减功能花销能够能够运用 lazy(LazyThreadSafetyMode.NONE){} 即可。

四、调查者托付

除了对特点的值进行托付,咱们甚至还能对调查到这个改变进程:

运用 observable 托付监听值的改变:

    var values: String by Delegates.observable("默许值") { property, oldValue, newValue ->
        YYLogUtils.w("打印值: $oldValue -> $newValue ")
    }
    values = "第一次修正"
    values = "第2次修正"
    values = "第三次修正"

打印:

Kotlin委托的原理与使用,以及在Android开发中常用的几个场景

咱们还能运用 vetoable 托付,和 observable 相同能够调查特点的改变,不同的是 vetoable 能够决定是否运用新值。

    var age: Int by Delegates.vetoable(18) { property, oldValue, newValue ->
        newValue > oldValue
    }
    YYLogUtils.w("age:$age")
    age = 14
    YYLogUtils.w("age:$age")
    age = 20
    YYLogUtils.w("age:$age")
    age = 22
    YYLogUtils.w("age:$age")
    age = 20
    YYLogUtils.w("age:$age")

咱们需求回来 booble 值觉得是否运用新值,比如上述的比如便是当新值大于老值的时分才赋值。那么打印的日志便是如下:

Kotlin委托的原理与使用,以及在Android开发中常用的几个场景

虽然这种办法咱们并不常用,一般咱们都是运用相似 Flow 之类的东西在源头就处理了逻辑,运用这种办法咱们就能够在特点的赋值进程中进行拦截了。在一些特定的场景下还是有用的。

五、Map托付

咱们的特点不止能够运用类的托付,推迟的托付,调查的托付,还能托付Map来进行赋值。

当特点的值与 Map 中 key 相同的时分,咱们能够把对应 key 的 value 取出来并赋值给特点:

class Member(private val map: Map<String, Any>) {
    val name: String by map
    val age: Int by map
    val dob: Long by map
    override fun toString(): String {
        return "Member(name='$name', age=$age, dob=$dob)"
    }
}

运用:

        val member = Member(mapOf("name" to "guanyu", "age" to 36, Pair("dob", 1234567890L)))
        YYLogUtils.w("member:$member")

打印的日志:

Kotlin委托的原理与使用,以及在Android开发中常用的几个场景

可是需求留意的是,map 中的 key 名字有必要要和特点的名字一致才行,不然托付后运行解析时会抛出 NoSuchElementException 反常提示。

例如咱们在 Member 方针中参加一个并不存在的 address 特点,再次运行就会报错。

Kotlin委托的原理与使用,以及在Android开发中常用的几个场景

而咱们把 Int 的 age 特点赋值给为字符串也会报类型转化反常:

Kotlin委托的原理与使用,以及在Android开发中常用的几个场景

所以一定要一一对应才行哦,我怎样感觉有一点 TypeScript 结构赋值的那味道 – – !

Kotlin委托的原理与使用,以及在Android开发中常用的几个场景

总结

托付虽好不要乱用。托付毕竟还是中间多了一个托付类,假如没必要能够直接赋值结束,而不需求多一个中间类占用内存。

咱们能够经过接口托付来结束一些可选的装备。经过托付类结束特点的监听与赋值。能够削减一些模板代码,到达低耦合高内聚的效果,能够提高程序的可维护性、可扩展性和可重用性。

关于特点的类托付,咱们能够将特点的读取和写入操作托付给另一个方针,或许另一个特点,或许运用推迟托付来推迟方针的创立直到第一次拜访。

关于 map 的托付,咱们需求细心对应特点与 key 的一致性。以免呈现过错,这是运行时的过错,有可能呈现在生产环境上的。

那么咱们都是怎样运用的呢?有没有更好的办法呢?或许你有遇到的坑也都能够在评论区交流一下,咱们能够互相学习前进。如有本文有一些错漏的地方,希望同学们能够指出。

假如感觉本文对你有一点点的协助,还望你能点赞支持一下,你的支持是我最大的动力。

本文的部分代码能够在我的 Kotlin 测试项目中看到,【传送门】。你也能够关注我的这个Kotlin项目,我有时间都会继续更新。

Ok,这一期就此结束。

Kotlin委托的原理与使用,以及在Android开发中常用的几个场景

本文正在参加「金石计划」