Kotlin修炼指南(五)—Delegates

2021-01-29 15:11:48 浏览数 (1)

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

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

类委托

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

类委托入门

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

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

代码语言:javascript复制
interface IDataPersistence {
    fun addData()
    fun delData()
    fun queryData()
}

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

代码语言:javascript复制
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关键字,将接口的实现,委托给一个具体的实例来作为自己的实现。

代码语言:javascript复制
class MyDB(private val delegate: IDataPersistence) : IDataPersistence by delegate

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

代码语言:javascript复制
// val myDB = MyDB(SQL())
val myDB = MyDB(SharedPreferences())
myDB.addData()
myDB.delData()
myDB.queryData()

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

代码语言:javascript复制
class MyDB(private val delegate: IDataPersistence) : IDataPersistence by delegate {
    override fun addData() {}

    override fun delData() {}

    override fun queryData() {}
}

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

代码语言:javascript复制
class MyDB : IDataPersistence by SQL()

调用:
MyDB().addData()

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

类委托的原理

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

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

类委托的使用场景

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

代码语言:javascript复制
class RedSquare : Shape by Square(), Color by Red() {
    fun draw() {
        print("draw Square with Red")
    }
}

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

代码语言:javascript复制
class NewList(private val list: MutableList<String>) : MutableList<String> by list {
    fun newFunction() {}
}

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

属性委托

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

代码语言:javascript复制
val/var <属性名>: <类型> by <表达式>

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

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

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

代码语言:javascript复制
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提供了两个接口来帮助开发者实现。

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

代码语言:javascript复制
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的操作。

代码语言:javascript复制
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方法,用于添加一些逻辑,代码如下所示。

代码语言:javascript复制
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方法,要处理的逻辑基本是一样的,即对字母做小写,所以我们对这个操作进行抽取,设置一个委托,代码如下所示。

代码语言:javascript复制
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类进行改造了,代码如下所示。

代码语言:javascript复制
class Person {
    var firstName: String by FormatDelegate()
    var lastname: String by FormatDelegate()
}

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

抽象属性委托的一般步骤

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

  • 青铜:抽取公共函数,在处理时对属性进行调用
  • 黄金:重新属性的get、set函数,将逻辑封装
  • 王者:使用属性委托,将逻辑抽取出来

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

代码语言:javascript复制
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函数内部,代码如下所示。

代码语言:javascript复制
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函数,所以,下面使用委托对这个逻辑再进行一次封装,代码如下所示。

代码语言:javascript复制
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

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

代码语言:javascript复制
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可以非常方便的帮助我们实现观察者模式,代码如下所示。

代码语言:javascript复制
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退出的功能,代码如下所示。

代码语言:javascript复制
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可以通过处理器函数来决定属性值是否生效,代码如下所示。

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

SharedPreferences操作简化

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

代码语言:javascript复制
PreferencesUtil.getInstance().putBoolean(XXXXX, false);

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

代码语言:javascript复制
@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 人点赞