Kotlin修炼指南(五)—Delegates
委托,是一种比较常见的设计模式,通常采用接口或者抽象类的方式来实现,在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代码来实现委托机制的。
实际上就是在调用者内部创建一个实现者的变量,在实现的接口方法中,变量调用该方法,从而实现调用,一切都只是语法糖而已,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) : MutableList 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提供了两个接口来帮助开发者实现。
所以上面的代码可以简写成下面这样。
class MyDelegate : ReadWriteProperty {
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 {
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 {
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 : ReadWriteProperty {
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 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(private val context: Context, private val propName: String, private val defaultValue: T) : ReadWriteProperty {
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 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 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 欢迎大家访问