请先关注 [低调大师] 公众号 优秀的自媒体个人博客,低调大师,许军

低调大师

您现在的位置是:首页>文章详情

文章详情

Kotlin修炼指南(五)—Delegates

2021-05-20 59热度

委托,是一种比较常见的设计模式,通常采用接口或者抽象类的方式来实现,在Java代码中,一般使用接口来进行封装,而在kotlin中,可以通过委托机制来实现更加方便的委托模式。

Kotlin中的委托分为两种——类委托与属性委托,其中属性委托,是Kotlin非常强大的一个语法糖,借助这个功能,我们可以消除很多重复的模板代码,将Kotlin的代码榨干到极致。

类委托

下面我们先通过一个简单的例子来了解下什么是类委托,以及类委托的具体作用。

类委托入门

在一般的业务开发中,我们经常会遇到这样的场景——一个业务功能,有多种实现,通过接口来封装具体的业务方法,通过实现接口来完成不同实现,这样的场景有很多,使用Kotlin来实现这一功能,步骤如下。

第一步:创建接口约束,抽象业务场景。例如下面这个数据持久化的例子,我们通过接口定义了三个数据操作方法。

interface IDataPersistence {     fun addData()     fun delData()     fun queryData() } 

第二步:创建委托的实现,实现约束接口。数据持久化有多种不同的实现方式,下面这就是简单的两种,一种是通过SQL进行持久化,另一种是通过SharedPreferences进行持久化。

class SQL : IDataPersistence {     override fun addData() {         Log.d("xys", "addData with SQL")     }     override fun delData() {         Log.d("xys", "delData with SQL")     }     override fun queryData() {         Log.d("xys", "queryData with SQL")     } } class SharedPreferences : IDataPersistence {     override fun addData() {         Log.d("xys", "addData with SharedPreferences")     }     override fun delData() {         Log.d("xys", "delData with SharedPreferences")     }     override fun queryData() {         Log.d("xys", "queryData with SharedPreferences")     } } 

第三步:调用约束接口,即业务方调用,但不用考虑具体的实现。类委托的语法格式是,<类>:<约束接口> by <实现类的实例>,即通过by关键字,将接口的实现,委托给一个具体的实例来作为自己的实现。

class MyDB(private val delegate: IDataPersistence) : IDataPersistence by delegate 

使用方式与Java代码通过接口来实现基本一致,即在类初始化的时候,传入具体的实现类即可。

// val myDB = MyDB(SQL()) val myDB = MyDB(SharedPreferences()) myDB.addData() myDB.delData() myDB.queryData() 

在Kotlin的类委托机制中,调用方和业务实现方,都需要实现约束接口,调用方只需要传入不同类型的业务实现方式,即可通过约束调用具体的实现。这一点看上去好像并没有比Java方便多少,但是在Kotlin中,在某些简单的场景下,实际上是可以省略掉实现类的,直接通过对委托实现的重写来实现委托接口,代码如下所示。

class MyDB(private val delegate: IDataPersistence) : IDataPersistence by delegate {     override fun addData() {}     override fun delData() {}     override fun queryData() {} } 

再简单一点,如果你不用传入多种不同的实例,可以在构造方法中去掉默认参数,直接在by关键字后面添加具体的接口实现,还是上面的例子,代码如下所示。

class MyDB : IDataPersistence by SQL() 调用: MyDB().addData() 

通过委托,可以在不影响继承(MyDB可以继承其它类)的情况下,通过委托,使用指定接口中的方法。

类委托的原理

通过反编译Kotlin实现的代码,可以很方便的了解Kotlin内部是如何通过Java代码来实现委托机制的。

watermark,size_16,text_QDUxQ1RP5Y2a5a6i,color_FFFFFF,t_100,g_se,x_10,y_10,shadow_90,type_ZmFuZ3poZW5naGVpdGk=

实际上就是在调用者内部创建一个实现者的变量,在实现的接口方法中,变量调用该方法,从而实现调用,一切都只是语法糖而已,Kotlin帮你简化了代码。

类委托的使用场景

通过类委托机制,可以很方便的实现多态。这是类委托最重要的使用场景,通过接口定义来实现多态性,同时使用by关键字来简化Java中接口实现的冗余代码,下面的这个简单的例子,就是一个最好的说明。

class RedSquare : Shape by Square(), Color by Red() {     fun draw() {         print("draw Square with Red")     } } 

另外,委托还可以用于在不修改原来代码及架构的基础上,对原有功能扩展或者修改。例如我们要对MutableList类拓展一个函数,如果是Java代码,或者不使用委托的Kotlin代码,你必须实现List接口中的所有函数,虽然你未作修改,只是单纯的传递调用,但是需要为这个拓展写很多无用的代码,而使用委托,则完全不用处理这些冗余,代码如下所示。

class NewList(private val list: MutableList<String>) : MutableList<String> by list {     fun newFunction() {} } 

Kotlin会自动在编译时帮你添加其它接口方法的默认实现。

属性委托

属性委托指的是一个类的某个属性值不是在类中直接进行定义,而是将其委托给一个代理类,从而实现对该类的属性统一管理,属性委托的一般格式如下所示。

val/var <属性名>: <类型> by <表达式> 

在前面的讲解中,类委托,委托的是接口中指定的方法,而属性委托,则委托的是属性的get、set方法,属性委托实际上就是将get、set方法的逻辑委托给一个单独的类来进行实现(对于val属性来说,委托的是getValue方法,对于var属性来说,委托的是setValue和getValue方法)。

属性委托在那些需要对属性的get、set方法复用逻辑的场景下,是非常方便的,下面通过一个简单的例子来演示下属性委托机制。

首先,我们定义一个var属性,并将其委托给MyDelegate类,即将get和set方法进行了交接托管,因此,MyDelegate类需要重写getValue和setValue方法,为其提供新的返回值和逻辑,代码如下所示。

var delegateProp by MyDelegate() class MyDelegate {     operator fun getValue(thisRef: Any?, property: KProperty<*>): String {         return "MyDelegate get $thisRef ${property.name}"     }     operator fun setValue(thisRef: Any?, property: KProperty<*>, value: String) {         Log.d("xys", "MyDelegate set $value $thisRef ${property.name}")     } } 调用: Log.d("xys", delegateProp) delegateProp = "abc" out: com.yw.demo D/xys: MyDelegate get com.yw.demo.MainActivity@595c528 delegateProp com.yw.demo D/xys: MyDelegate set abc com.yw.demo.MainActivity@595c528 delegateProp 

这样处理之后,我们在使用delegateProp这个属性的时候,就会自动拓展MyDelegate中的处理。

不过呢,这样写起来太麻烦,MyDelegate中的方法都需要手动来实现,所以Kotlin提供了两个接口来帮助开发者实现。

watermark,size_16,text_QDUxQ1RP5Y2a5a6i,color_FFFFFF,t_100,g_se,x_10,y_10,shadow_90,type_ZmFuZ3poZW5naGVpdGk=

所以上面的代码可以简写成下面这样。

class MyDelegate : ReadWriteProperty<Any, String> {     override fun setValue(thisRef: Any, property: KProperty<*>, value: String) {         Log.d("xys", "MyDelegate set $value $thisRef ${property.name}")     }     override fun getValue(thisRef: Any, property: KProperty<*>): String {         return "MyDelegate get $thisRef ${property.name}"     } } 

属性委托使用场景

那么这东西有什么用呢,下面举个例子。

逻辑封装

例如对参数进行encode的操作。

object Prop {     var encodeProp: String by EncodeProperty("init") } class EncodeProperty(var value: String) : ReadWriteProperty<Prop, String> {     override fun getValue(thisRef: Prop, property: KProperty<*>): String {         return "get encode prop output $value"     }     override fun setValue(thisRef: Prop, property: KProperty<*>, value: String) {         this.value = value         Log.d("xys", "save encode prop $value")     } } 调用: Prop.encodeProp = "xuyisheng" Log.d("xys", Prop.encodeProp) 

参数依然是那个参数变量,但是对它的处理被外包出去,交给了EncodeProperty来进行处理,这里的实现就是业务需要的encode操作,将来如果encode操作有改动,那么只需要修改EncodeProperty即可。也就是说,我们将encode的具体逻辑进行了封装,这样便于拓展和维护。

消除模板代码

再来看下面这个例子,Person类中有两个属性,分别修改了set方法,用于添加一些逻辑,代码如下所示。

class Person {     var firstName: String = ""         set(value) {             field = value.toLowerCase()         }     var lastname: String = ""         set(value) {             field = value.toLowerCase()         } } 调用: val person = Person() person.firstName = "XU" person.lastname = "YISHENG" println("${person.firstName} ${person.lastname}") 

但是这里的两个属性的set方法,要处理的逻辑基本是一样的,即对字母做小写,所以我们对这个操作进行抽取,设置一个委托,代码如下所示。

class FormatDelegate : ReadWriteProperty<Any?, String> {     private var formattedString: String = ""     override fun getValue(thisRef: Any?, property: KProperty<*>): String {         return formattedString     }     override fun setValue(thisRef: Any?, property: KProperty<*>, value: String) {         formattedString = value.toLowerCase()     } } 

这个委托做的事情,和在前面的代码中set的逻辑是一样的。那么这个时候,就可以对Person类进行改造了,代码如下所示。

class Person {     var firstName: String by FormatDelegate()     var lastname: String by FormatDelegate() } 

这样就将同样的set操作的逻辑,封装在了FormatDelegate中,从而实现了模板代码的消除。

抽象属性委托的一般步骤

从上面的例子我们可以发现,其实只要是对属性的get、set方法有操作的地方,几乎都可以使用属性委托来简化,对于这种操作,开发者一般会经历下面几个过程。

  • 青铜:抽取公共函数,在处理时对属性进行调用

  • 黄金:重新属性的get、set函数,将逻辑封装

  • 王者:使用属性委托,将逻辑抽取出来

下面再通过一个实例,来演示下这个步骤。我们以Fragment的启动方式为例来讲解,经常有写类似的代码来处理Fragment的启动。

const val PARAM1 = "param1" const val PARAM2 = "param2" class DemoFragment : Fragment() {     private var param1: Int? = null     private var param2: String? = null     override fun onCreate(savedInstanceState: Bundle?) {         super.onCreate(savedInstanceState)         arguments?.let { args ->             param1 = args.getInt(PARAM1)             param2 = args.getString(PARAM2)         }     }     companion object {         fun newInstance(param1: Int, param2: String): DemoFragment =             DemoFragment().apply {                 arguments = Bundle().apply {                     putInt(PARAM1, param1)                     putString(PARAM2, param2)                 }             }     } } 

首先,我们可以通过Kotlin的set、get函数进行改写,将arguments的填充,放到属性的get、set函数内部,代码如下所示。

class DemoFragment : Fragment() {     private var param1: Int?         get() = arguments?.getInt(PARAM1)         set(value) {             value?.let {                 arguments?.putInt(PARAM1, it)             }         }     private var param2: String?         get() = arguments?.getString(PARAM2)         set(value) {             arguments?.putString(PARAM2, value)         }     companion object {         fun newInstance(param1: Int, param2: String): DemoFragment =             DemoFragment().apply {                 this.param1 = param1                 this.param2 = param2             }     } } 

但是我们还是要为每个属性写重复的代码,特别是当属性很多的时候,每个属性都要写重复的put、get函数,所以,下面使用委托对这个逻辑再进行一次封装,代码如下所示。

class DemoFragment : Fragment() {     private var param1: Int by FragmentArgumentDelegate()     private var param2: String by FragmentArgumentDelegate()     companion object {         fun newInstance(param1: Int, param2: String): DemoFragment =             DemoFragment().apply {                 this.param1 = param1                 this.param2 = param2             }     } } @Suppress("UNCHECKED_CAST") class FragmentArgumentDelegate<T : Any> : ReadWriteProperty<Fragment, T> {     override fun getValue(thisRef: Fragment, property: KProperty<*>): T {         val key = property.name         return thisRef.arguments?.get(key) as T     }     override fun setValue(thisRef: Fragment, property: KProperty<*>, value: T) {         val arguments = thisRef.arguments         val key = property.name         arguments?.put(key, value)     } } fun <T> Bundle.put(key: String, value: T) {     when (value) {         is Boolean -> putBoolean(key, value)         is String -> putString(key, value)         is Int -> putInt(key, value)         is Short -> putShort(key, value)         is Long -> putLong(key, value)         is Byte -> putByte(key, value)         is ByteArray -> putByteArray(key, value)         is Char -> putChar(key, value)         is CharArray -> putCharArray(key, value)         is CharSequence -> putCharSequence(key, value)         is Float -> putFloat(key, value)         is Bundle -> putBundle(key, value)         is Parcelable -> putParcelable(key, value)         is Serializable -> putSerializable(key, value)         else -> throw IllegalStateException("Type of property $key is not supported")     } } 

这里要注意的是,Bundle没有提供单个属性的put拓展,所以我们需要自己实现一个。

通过上面的这些操作,就将Fragment参数传递的代码简化到了只有一行,其它任何的Fragment传参,都可以使用这个委托。

委托操作实例

最后,再介绍一些官方推荐的委托使用场景。

内置委托函数

Kotlin系统库提供了很多有用的委托,这些都内置在Delegate库中。

延迟属性lazy

属性委托,可以是一个表达式,借助这个特性,可以实现属性的延迟加载,即在第一次访问的时候进行初始化。

private val lazyProp: String by lazy {     Log.d("xys", "表达式只会执行一次")     "执行后赋值给lazyProp" } Log.d("xys", lazyProp) Log.d("xys", lazyProp) out: D/xys: 表达式只会执行一次 D/xys: 执行后赋值给lazyProp D/xys: 执行后赋值给lazyProp 

要注意的是,lazy表达式中的代码,只会在第一次初始化的时候调用,之后就不会调用了,所以这里log只打印了一次。

观察属性observable

Delegates.observable可以非常方便的帮助我们实现观察者模式,代码如下所示。

var observableProp: String by Delegates.observable("init value 0") { property, oldValue, newValue ->     Log.d("xys", "change: $property: $oldValue -> $newValue ") } Log.d("xys", observableProp) observableProp = "change value" 

当属性值发生改变的时候,就会通知出来。

借助观察属性,可以很方便的实现时间差的判断,例如连续back退出的功能,代码如下所示。

private var backPressedTime by Delegates.observable(0L) { pre, old, new ->     if (new - old < 2000) {         finish()     } else {         Toast.makeText(this, "再按一次返回退出", Toast.LENGTH_SHORT).show()     } } override fun onBackPressed() {     backPressedTime = System.currentTimeMillis() } 

条件观察属性vetoable

vetoable 与 observable一样,可以观察属性值的变化,不同的是,vetoable可以通过处理器函数来决定属性值是否生效,代码如下所示。

var vetoableProp: Int by Delegates.vetoable(0){     _, oldValue, newValue ->     // 如果新的值大于旧值,则生效     newValue > oldValue } 

SharedPreferences操作简化

前面我们提到了,只要是涉及到get、set方法的使用的场景,几乎都可以使用委托来进行优化,再拓展一下,凡是对属性有进行读写操作的,都可以使用委托来进行优化,例如我们在Android中比较常用的SharedPreferences操作,大部分情况下,都会抽取工具类,类似下面这样进行调用。

PreferencesUtil.getInstance().putBoolean(XXXXX, false); 

下面通过委托,我们可以将一个普通属性的读写进行代理,代理到通过SP读写,这样我们在代码中对这个属性的读写,实际上是将其代理到SP中,代码如下所示。

@Suppress("UNCHECKED_CAST") class PreferenceDelegate<T>(private val context: Context, private val propName: String, private val defaultValue: T) : ReadWriteProperty<Any, T> {     private val sharedPreferences: SharedPreferences by lazy { context.getSharedPreferences("SP_NAME", Context.MODE_PRIVATE) }     override fun setValue(thisRef: Any, property: KProperty<*>, value: T) {         value?.let { putSPValue(propName, value) }     }     override fun getValue(thisRef: Any, property: KProperty<*>): T {         return getSPValue(propName, defaultValue) ?: defaultValue     }     private fun <T> getSPValue(name: String, defaultValue: T): T? = with(sharedPreferences) {         val result = when (defaultValue) {             is String -> getString(name, defaultValue)             is Int -> getInt(name, defaultValue)             is Long -> getLong(name, defaultValue)             is Float -> getFloat(name, defaultValue)             is Boolean -> getBoolean(name, defaultValue)             else -> null         }         result as T     }     private fun <T> putSPValue(name: String, value: T) = with(sharedPreferences.edit()) {         when (value) {             is Long -> putLong(name, value)             is String -> putString(name, value)             is Int -> putInt(name, value)             is Boolean -> putBoolean(name, value)             is Float -> putFloat(name, value)             else -> null         }     }?.apply() } 使用: var valueInSP: String by PreferenceDelegate(this, "test", "init") Log.d("xys", valueInSP) valueInSP = "new value" Log.d("xys", valueInSP) out: D/xys: init D/xys: new value 

通过上面的操作,我们在使用SharedPreferences的时候,只需要对某个要操作的属性使用by进行标记,将其委托给PreferenceDelegate即可,这样表面上好像是在操作一个String,但实际上,已经是对SharedPreferences的操作了。

在下面这个lib中,对很多场景下的委托进行了封装,大家可以参考下它的实现。

https://github.com/fengzhizi715/SAF-Object-Delegate

向大家推荐下我的网站 https://xuyisheng.top/  点击原文一键直达

专注 Android-Kotlin-Flutter 欢迎大家访问

收藏 (0)

相关文章

    文章评论

    共有0条评论来说两句吧...