Android开发之Kotlin委托的原理与使用详解

目录
  • 前言
  • 一、接口/类委托
  • 二、属性委托
  • 三、延迟委托
  • 四、观察者委托
  • 五、Map委托
  • 总结

前言

在设计模式中,委托模式(Delegate Pattern)与代理模式都是我们常用的设计模式(Proxy Pattern),两者非常的相似,又有细小的区分。

委托模式中,委托对象和被委托对象都是同一类型的对象,委托对象将任务委托给被委托对象来完成。委托模式可以用于实现事件监听器、回调函数等功能。

代理模式中,代理对象与被代理对象是两种不同的对象,代理对象代表被代理对象的功能,代理对象可以控制客户对被代理对象的访问。代理模式可以用于实现远程代理、虚拟代理、安全代理等功能。

以类的委托与代理来举例,委托对象和被委托对象都实现了同一个接口或继承了同一个类,委托对象将任务委托给被委托对象来完成。代理模式中,代理对象与被代理对象实现了同一个接口或继承了同一个类,代理对象代表被代理对象,客户端通过代理对象来访问被代理对象。

两者的区别:

他们虽然都有同一个接口,主要区别在于委托模式中委托对象和被委托对象是同一类型的对象,而代理模式中代理对象与被代理对象是两种不同的对象。总的来说,委托模式是为了将方法的实现交给其他类去完成,而代理模式则是为了控制对象的访问,并在访问前后进行额外的操作。

而我们常用的委托模式怎么使用?在 Java 语言中需要我们手动的实现,而在 Kotlin 语言中直接通过关键字 by 就可以实现委托,其实现更加优雅、简洁了。

我们在开发一个 Android 应用中,常用到的委托分为:

  • 接口/类的委托
  • 属性的委托
  • 结合lazy的延迟委托
  • 观察者的委托
  • 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()
    }

打印日志如下:

其实在 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 "我是赋值给与的文本"
        }

    }

打印的结果:

而我们定义一个可读写的属性则可以

  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"

打印则如下:

为了怕大家写错,我们其实可以用接口来限制,只读的和读写的属性,我们分别可以用 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)

只有在第一次使用此属性的时候才会初始化,一旦初始化之后就可以直接获取到值。

日志打印:

它的内部其实也是使用的是类的委托实现。

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的参数分为三种类型:

  • SYNCHRONIZED:添加同步锁,使lazy延迟初始化线程安全
  • PUBLICATION:初始化的lambda表达式,可以在同一时间多次调用,但是只有第一次的返回值作为初始化值
  • NONE:没有同步锁,非线程安全

默认情况下,对于 lazy 属性的求值是同步锁的(synchronized),是可以保证线程安全的,但是如果不需要线程安全和减少性能花销可以可以使用 lazy(LazyThreadSafetyMode.NONE){} 即可。

四、观察者委托

除了对属性的值进行委托,我们甚至还能对观察到这个变化过程:

使用 observable 委托监听值的变化:

    var values: String by Delegates.observable("默认值") { property, oldValue, newValue ->

        YYLogUtils.w("打印值: $oldValue -> $newValue ")
    }

    values = "第一次修改"
    values = "第二次修改"
    values = "第三次修改"

打印:

我们还能使用 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 值觉得是否使用新值,比如上述的例子就是当新值大于老值的时候才赋值。那么打印的日志就是如下:

虽然这种方式我们并不常用,一般我们都是使用类似 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")

打印的日志:

但是需要注意的是,map 中的 key 名字必须要和属性的名字一致才行,否则委托后运行解析时会抛出 NoSuchElementException 异常提示。

例如我们在 Member 对象中加入一个并不存在的 address 属性,再次运行就会报错。

而我们把 Int 的 age 属性赋值给为字符串也会报类型转换异常:

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

总结

委托虽好不要滥用。委托毕竟还是中间多了一个委托类,如果没必要可以直接赋值实现,而不需要多一个中间类占用内存。

我们可以通过接口委托来实现一些可选的配置。通过委托类实现属性的监听与赋值。可以减少一些模板代码,达到低耦合高内聚的效果,可以提高程序的可维护性、可扩展性和可重用性。

对于属性的类委托,我们可以将属性的读取和写入操作委托给另一个对象,或者另一个属性,或者使用延迟委托来推迟对象的创建直到第一次访问。

对于 map 的委托,我们需要仔细对应属性与 key 的一致性。以免出现错误,这是运行时的错误,有可能出现在生产环境上的。

那么大家都是怎么使用的呢?有没有更好的方式呢?或者你有遇到的坑也都可以在评论区交流一下,大家可以互相学习进步。如有本文有一些错漏的地方,希望同学们可以指出。

到此这篇关于Android开发之Kotlin委托的原理与使用详解的文章就介绍到这了,更多相关Android Kotlin委托内容请搜索我们以前的文章或继续浏览下面的相关文章希望大家以后多多支持我们!

(0)

相关推荐

  • Kotlin开发笔记之委托属性与区间(译)

    前言 本文主要给大家介绍了关于Kotlin委托属性与区间的相关内容,分享出来供大家参考学习,下面话不多说了,来一起看看详细的介绍吧. 委托属性 有一些常见的属性类型,虽然我们可以在每次需要的时候手动实现它们, 但是如果能够为大家把他们只实现一次并放入一个库会更好.例如包括 延迟属性(lazy properties): 其值只在首次访问时计算, 可观察属性(observable properties): 监听器会收到有关此属性变更的通知, 把多个属性储存在一个映射(map)中,而不是每个存在单独的

  • 关于Kotlin委托你必须重视的几个点

    目录 前言 一.委托类 二. 委托属性 2.1 自定义委托 三.委托进阶 3.1 懒加载委托 3.2 Delegates.observable 观察者委托 3.3 by map 映射委托 3.4 两个属性之间的直接委托 3.5 提供委托 四.委托栗子 4.1 简化Fragment / Activity 传参 4.2 简化SharedPreferences存取值 4.3 数据与View的绑定 五.小结 参考 总结 前言 委托模式是实现继承的一个很好的替代方式,也是Kotlin语言的一种特性,可以很

  • Android开发之Kotlin委托的原理与使用详解

    目录 前言 一.接口/类委托 二.属性委托 三.延迟委托 四.观察者委托 五.Map委托 总结 前言 在设计模式中,委托模式(Delegate Pattern)与代理模式都是我们常用的设计模式(Proxy Pattern),两者非常的相似,又有细小的区分. 委托模式中,委托对象和被委托对象都是同一类型的对象,委托对象将任务委托给被委托对象来完成.委托模式可以用于实现事件监听器.回调函数等功能. 代理模式中,代理对象与被代理对象是两种不同的对象,代理对象代表被代理对象的功能,代理对象可以控制客户对

  • Android开发之HttpClient异步请求数据的方法详解【附demo源码下载】

    本文实例讲述了Android开发之HttpClient异步请求数据的方法.分享给大家供大家参考,具体如下: 前面一篇Android开发笔记之:AsyncTask的应用较为详细的讲述了Asynctask的原理与应用,这里来结合使用一下HttpClient与Asynctask~ 代码编写如下: 服务器代码我就不写出来了,就是一个用户名和密码~ 1.写一个类HttpClientUtil,来实现HttpClient对象的创建并且返回HttpResponse对象 public class HttpClie

  • java数据库开发之JDBC基础使用方法及实例详解

    1.什么是JDBC JDBC是一种用于执行SQL语句的Java API,可以为多种关系数据库提供统一访问,它由一组用Java语言编写的类和接口组成.JDBC提供了一种基准,据此可以构建更高级的工具和接口,使数据库开发人员能够编写数据库应用程序 JDBC 数据库访问规范 应用程序 <-> JDBC <-> MySQL驱动 <-> MySQL                  <-> Oracle驱动 <-> Oracle 导入jar包 加载驱动 C

  • iOS开发之UIPickerView实现城市选择器的步骤详解

    前言 UIPickerView是一个选择器控件,它可以生成单列的选择器,也可生成多列的选择器,而且开发者完全可以自定义选择项的外观,因此用法非常灵活.UIPickerView直接继承了UIView,没有继承UIControl,因此,它不能像UIControl那样绑定事件处理方法,UIPickerView的事件处理由其委托对象完成. 本文借助于UIPickerView来实现城市选择器,第一列为省份,第二列为第一列省份对应的城市或者区,数据放在plist中,plist结构如下图所示,第一层是一个Di

  • python图形界面开发之wxPython树控件使用方法详解

    wxPython树控件介绍 树(tree)是一种通过层次结构展示信息的控件,如下图所示是树控件示例,左窗口中是树控件,在wxPython中树控件类是wx.TreeCtrl. wx.TreeCtrl常用的方法有 AddRoot(text, image=-1, selImage=-1, data=None).添加根节点,text参数根节点显示的文本:image参数是该节点未被选中时的图片索引,wx.TreeCtrl中使用的图片被放到wx.ImageList图像列表中:selImage参数是该节点被选

  • Android开发之Gradle 进阶Tasks深入了解

    目录 前言 定义Task register与create的区别 查找Task 配置Task 将参数传递给Task构造函数 Task添加依赖 Task排序 Task添加说明 跳过Task 使用onlyIf 使用 StopExecutionException 禁用与启用Task Task超时 Task支持增量编译 Task的输入输出 自定义task类型 声明输入输出的好处 推断task依赖关系 输入和输出验证 并行task 增量编译原理解析 一些高端操作 将@OutputDirectory链接到@I

  • Android开发之ContentProvider的使用详解

    前言         Content Provider为存储数据和获取数据提供了统一的接口,它可以完成在不同应用程序下的数据共享,而在上一篇文章Android开发之SQLite的使用方法讲到的SQLite只能在同一个程序中共享数据.另外android为一些常见的数据,比如说音频,视频,图片,通讯录等提供了Content Provider,这样我们就可以很方便的对这些类型的数据操作了.使用ContentProvider的好处是开发人员不需要考虑数据内部是怎么存储的,比如说如果我们想利用Conten

  • Android开发之AAR文件的生成与使用步骤

    目录 前言 一.AAR是什么? 二.使用步骤 1.生成AAR 2.AAR使用 附:注意事项 总结 前言 现在App开发组件化技术已是常态,有很多的功能模块都被抽出来成为一个个组件供给开发者使用.为了开发者使用,这些组件都会被打包,就和java中的库一样.在java中,一个模块可以被打包为Jar包,而在Android中,不仅仅有java文件,还有一些其他的资源文件,所以就出现了AAR文件(一种打包格式),本文通过一个demo来介绍如何打包一个Library文件,生成AAR. 一.AAR是什么? a

  • Android开发之Button事件实现与监听方法总结

    本文实例总结了Android开发之Button事件实现与监听方法.分享给大家供大家参考,具体如下: 先来介绍Button事件实现的两种方法 main.xml <?xml version="1.0" encoding="utf-8"?> <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" android:orientation="

  • Android开发之button事件监听简单实例

    本文实例讲述了Android开发之button事件监听用法.分享给大家供大家参考.具体如下: 事件监听的listener,有以下几种方式: 1.声明一个普通的class,实现OnClickListener接口,然后在button的setOnClickListener中new该类的一个对象. 2.使用匿名内部类,直接 btn.setOnClickListener(new OnClickListener() { @Override public void onClick(View arg0) { S

随机推荐