kotlin委托的本质以及mmkv的应用(代码片段)

涂程 涂程     2023-03-09     782

关键词:

作者:DylanCai

前言

很多人写 Kotlin 只用到一些 Java 已有的东西,单纯地把 Java 代码翻译成 Kotlin 代码,而 Kotlin 一些好用的语法糖都没有用过。本文给大家介绍一个 Java 不常见但是很好用的 Kotlin 语法糖 —— Kotlin 委托。

Kotlin 委托包含了属性委托,是我个人很喜欢的一个特性。本文会用尽可能简单通俗的语言讲清楚委托到底是什么东西,什么实现属性委托。属性委托特别适合一些保存数据的场景,所以后面会给大家分享个人封装 MMKV 的思路,来实战一下属性委托。

接下来为大家讲解 Kotlin 委托的本质和 MMKV 的封装思路。

Kotlin 委托的本质

什么是委托

讲 Kotlin 的委托之前,要先讲一下委托模式。委托模式又称代理模式,是常见的设计模式。

委托模式类似于我们生活中的代理、代购、中介。有些东西我们很难直接去买或者不知道怎么去买,但是我们能通过代理、代购、中介等方式间接去购买,这样我们也有具有了购买该东西的能力。

那代码怎么实现呢?首先我们定义一个接口,声明一个购买方法:

interface Subject 
  fun buy()

然后创建一个代理类实现该购买功能:

class Delegate : Subject 
  override fun buy() 
    print("I can buy it because I live abroad.")
  

在某个类需要该功能但是没法直接实现的时候,就能通过代理间接地实现功能。

class RealSubject : Subject 

  private val delegate: Subject = Delegate()

  override fun buy() 
    delegate.buy()
  
 

总结一下,委托(代理)模式其实就是将接口功能交给另一个接口实例对象去实现。所以委托模式是有模板代码的,每个接口方法调用了对应的代理对象方法。

虽然存在模板代码,但是 Java 没有什么好的办法能生成模板代码, 而 Kotlin 可以零模板代码地原生支持它。通过 by 关键字进行委托,我们就能把上面的代码改成:

class RealSubject : Subject by Delegate()

这个委托的代码最终会生成上面的代码,简而言之就是编译器帮我们生成了模板代码。

另外 by 关键字后面的表达式可能会有很多写法:

class RealSubject(delegate: Subject) : Subject by delegate

class RealSubject : Subject by globalDelegate

class RealSubject : Subject by GlobalDelegate

class RealSubject : Subject by delegate ...

虽然写法有很多种,但是记住一点, by 后面的表达式一定是得到一个接口的实例对象。因为接口功能是要委托给一个具体的实例对象,而这个对象可能通过构造函数、顶级属性、单例、方法等方式得到。

对此不了解的话,看到 by 后面各式各样的写法会很懵。其实不是什么 Kotlin 语法,只是为了得到个对象而已。

什么是属性委托

接口是把接口方法委托出去,那属性要委托什么呢?其实也很容易想到,属性能把 get、set 方法委托出去。

val delegate = Delegate()
var message: String
  get() = delegate.getValue()
  set(value) = delegate.setValue(value)

Kotlin 支持用 by 关键字将属性委托给一个实例对象,这就是 Kotlin 的属性委托:

var message: String by Delegate()

当然属性的委托类不能随便写,有一套具体的规则。先来看一个委托类的示例:

class Delegate 

  operator fun getValue(thisRef: Any?, property: KProperty<*>): String =
    "$thisRef, thank you for delegating '$property.name' to me!"

  operator fun setValue(thisRef: Any?, property: KProperty<*>, value: String) =
    println("$value has been assigned to '$property.name' in $thisRef.")

可能有些人看了就懵了,怎么这么多东西,为什么要这么写?

其实是有一套固定的模板,不过不用特地去背,因为官方提供了接口类给我们快速实现。

public interface ReadWriteProperty<in T, V> : ReadOnlyProperty<T, V> 

  public override operator fun getValue(thisRef: T, property: KProperty<*>): V

  public operator fun setValue(thisRef: T, property: KProperty<*>, value: V)

但是这个接口并不是必须的,我们手敲出对应的方法也能进行属性委托,接下来我会和大家讲清楚方法里的每一个要点。

首先来看一下方法的第一个参数 thisRef,顾名思义这是 this 的引用。因为这个属性可能是在一个类里面,可能需要调用该类的方法,这时如果连外层的 this 引用都拿不到,那还谈何委托。不过我们可能用不到外层的类,这就可以像上面的示例那样定义为 Any? 类型。

然后是第二个参数 property,必须是 KProperty<*> 类型或其超类型。这个参数可以拿到属性的信息,比如属性的名字。为什么要这个参数呢?因为可能要根据不同的属性实现不同的业务。举个例子,假如我们是做房地产中介,我们不可能什么人都推荐一样的房子,有钱人可能要买别墅的。我们会根据不同的客户信息反馈不同的结果,这就需要拿到客户的资料。同理属性的委托类需要能拿到属性的信息。

还有最重要的一点,需要有 operator 关键字修饰的 getValue()setValue() 方法。这就涉及到另一个 Kotlin 的进阶用法——重载操作符。先讲个场景让大家来了解重载操作符,比如我们写了一个类,我们并不能像 Int 类型那样使用 a + b 的方式把两个对象相加。但是重载操作符能帮我们做到这事,我们增加 operator 关键字修饰的 plus() 方法之后就能 a + b 了。还能重载很多操作符,如 a++a > b 等,大家有兴趣自行去了解。

能重载的方法名是固定的,比如重载 plus() 方法就对应加法操作。而重载 getValue()setValue() 方法是对应该类的 get、set 方法。属性委托就是把属性的 get、set 方法委托给代理类的 get、set 方法。

以上都是一个属性委托的必要条件,你可能不用,但是你不能没有。只要委托实例不满足条件,编译器就会报错。

Kotlin 标准库还提供了几种常用的标准委托,方便我们在一些常用的场景快速实现属性委托。

延时委托

用于延时初始化的场景。因为委托的逻辑是固定的,官方已经帮我们写好了委托类代码,提供了一个 lazy() 方法快速创建委托类的实例。

val loadingDialog by lazy  LoadingDialog(this) 

首次获取属性的值会执行返回 lambda 表达式的结果,后续获取属性都是直接拿缓存。其实就是执行了以下的逻辑。

private var _loadingDialog: LoadingDialog? = null
val loadingDialog: LoadingDialog
  get() 
    if (_loadingDialog == null) 
      _loadingDialog = LoadingDialog(this)
    
    return _loadingDialog!!
  

lazy() 方法返回的是 Lazy 类的对象,那么编译器生成的委托类会是 Lazy 而不是前面的 ReadWriteProperty

val delegate: Lazy = SynchronizedLazyImpl<LoadingDialog>(...)
val loadingDialog: LoadingDialog
  get() = delegate.value

可观察的委托

能够很方便地实现观察者模式。委托类的代码也是固定的,所以官方提供了 Delegates.observable() 方法创建委托的实例。每次设置属性的值都能在 lambda 表达式接收到回调。

var name: String by Delegates.observable("<no name>")  prop, old, new ->
  println("$old -> $new")

该方法返回一个 ObservableProperty 对象,继承自 ReadWriteProperty。内部实现很简单,就是在 setValue() 的时候执行回调方法。

委托映射的属性值

简单来说就是把一个属性委托给 map。

class User(val map: Map<String, Any?>) 
  val name: String by map

获取上面的属性其实是用 map 获取键名为 name 的值,编译器会生成以下逻辑。

class User(val map: Map<String, Any?>) 
  val name: String 
    get() = map["name"] as String

属性委托给属性

听起来有点绕,讲一个我遇过的问题来让大家更好地理解这个特性。Jetpack MVVM 架构是让数据写在 Repository 层,Activity 需要通过 ViewModel 去操作 Repository 的数据。所以我之前写了以下逻辑:

object DataRepository 
  var isFirstLaunch: Boolean by MMKVDelegate()


class GuideViewModel : ViewModel() 
  var isFirstLaunch = DataRepository.isFirstLaunch


// In GuideActivity.kt
viewModel.isFirstLaunch = false

简单来说就是 ViewModel 转发了 Repository 属性委托的属性,但代码执行下来并没有调用属性委托的 set() 方法,导致数据没保存到。这段代码看似没什么问题,实则是有陷阱的。

问题出在 ViewModel,等号的写法是有问题的,那只是个赋值操作,并不是持有引用,所以修改 ViewModel 的属性不会影响到 Repository 的属性。我当时发现问题后改为了下面的写法:

class GuideViewModel : ViewModel() 
  var isFirstLaunch: Boolean
    get() = DataRepository.isFirstLaunch
    set(value) 
      DataRepository.isFirstLaunch = value
    

逻辑上没什么问题了,但是代码看起来很丑陋。后来发现 Kotlin 在 1.4 版本新增了特性来应对这种情况,能将一个属性委托给另一个属性,这样就完美解决了。

class GuideViewModel : ViewModel() 
  var isFirstLaunch by DataRepository::isFirstLaunch

小结

Kotlin 委托其实就是使用 by 语法后,编译器会帮我们生成了委托类的代码。

如果是接口的委托:

class RealSubject : Subject by Delegate()

编译器就会帮我们生成委托模式的代码:

class RealSubject : Subject 

  private val delegate: Subject = Delegate()

  override fun buy() 
    delegate.buy()
  
 

如果是属性的委托:

var name: String by PropertyDelegate()

编译器就会帮我们把属性的 get、set 方法委托出去:

val delegate = PropertyDelegate()
var name: String
  get() = delegate.getValue()
  set(value) = delegate.setValue(value)

by 关键字后面的表达式可能有各种各样的写法,但一定是返回一个委托的实例。

封装 MMKV

MMKV 很合适属性委托的场景,不过我们不可能只用属性委托进行封装,下面带大家完整地封装一下 MMKV

MMKV 初始化

每次都要在 Application 调用初始化方法有点繁琐,我们可以使用 App Startup 实现自动初始化。

dependencies 
    implementation "androidx.startup:startup-runtime:1.1.0"


class MMKVInitializer : Initializer<Unit> 

  override fun create(context: Context) 
    MMKV.initialize(context)
  

  override fun dependencies() = emptyList<Class<Initializer<*>>>()


<provider
  android:name="androidx.startup.InitializationProvider"
  android:authorities="$applicationId.androidx-startup"
  android:exported="false"
  tools:node="merge">
  <meta-data
    android:name="com.dylanc.mmkv.MMKVInitializer"
    android:value="androidx.startup" />
</provider>

如果有需要自定义 MMKV 的根目录,可以配置 App Startup 移除 MMKVInitializer。这样就不执行默认的初始化操作,然后根据需要手动在 Application 初始化即可。

管理 MMKV 实例

很多人会封装个工具类把获取 MMKV 实例的过程简化了,这样就能直接调用 encode()decode() 方法。

object MMKVUtils 
  private val kv = MMKV.defaultMMKV()

  fun encode(key: String, value: Boolean) 
    kv.encode(key, value)
  

  fun decodeBool(key: String, value: Boolean): Boolean 
    return kv.decodeBool(key, value)
  

  fun encode(key: String, value: Int) 
    kv.encode(key, value)
  

  // ...

MMKVUtils.encode("bool", true)
val bValue = MMKVUtils.decodeBool("bool")

这种封装有个弊端是写死了默认的 MMKV 实例,然而 MMKV 是可以使用多个实例的,比如不同业务需要区别存储:

val kv = MMKV.mmkvWithID("MyID")

这个可以避免组件化项目的不同模块使用了同样的 key 值导致数据冲突的问题,比如 A、B 模块保存位置都用 position 作为 key 值,这样数据会有冲突。如果使用了不同的 MMKV 实例,即使键名一样也不会有问题。

MMKV 还能配置多进程访问或者加密,直接写死用默认实例并不好,应该要能配置。

下面给出个人的封装方案:

val kv: MMKV = MMKV.defaultMMKV()

interface MMKVOwner 
  val kv: MMKV get() = com.dylanc.mmkv.kv // 因为属性重名,要加包名声明是上面的顶级属性

只需这么几行代码,就能完美替代上面的工具类。

首先我们声明了一个 MMKV 的顶级属性,顶级属性能在任意地方获取,因为是最高级别的。就这么一行代码就已经做到上面那个工具类的所有事。

kv.encode("bool", true)
val bValue = kv.decodeBool("bool")
kv.clearAll()

其次我们还声明了一个 MMKVOwner 接口,可以在需要区别存储的时候,实现该接口,重写 kv 属性。这样就能在不影响原有代码的情况下,把该类的 MMKV 实例给替换了。

object UserRepository : MMKVOwner 
  override val kv: MMKV = MMKV.mmkvWithID("user")

只需简单几行代码的封装,即保留了 MMKV 的所有功能,又能灵活切换 MMKV 实例。

使用属性委托

上面是获取 MMKV 实例的封装,还不够易用。我们能运用前面讲的属性委托进一步优化保存和读取的代码。

创建一个类实现 ReadWriteProperty 接口,在 getValue()setValue() 方法调用 MMKV 的编码解码方法,key 值使用 property.name

class MMKVBoolProperty : ReadWriteProperty<MMKVOwner, Boolean> 
  override fun getValue(thisRef: MMKVOwner, property: KProperty<*>): Boolean =
    thisRef.kv.decodeBool(property.name)

  override fun setValue(thisRef: MMKVOwner, property: KProperty<*>, value: Boolean) 
    thisRef.kv.encode(property.name, value)
  

注意 thisRef 的类型是故意定为 MMKVOwner 而不是 Any?,也就是说必须实现了 MMKVOwner 接口才能使用 MMKV 属性委托。

为什么要这么做呢?首先你需要拥有了 MMKV 才能把属性委托给 MMKV ,从逻辑上来说更加合理。其次如果能随意写 MMKV 属性委托的话,容易出现在多个类里写了同一个值的属性委托,一旦属性名敲错,数据就异常了,这是不推荐的用法。所以要求必须实现 MMKVOwner 接口,这样在多个类使用属性委托就变得更麻烦。

推荐用法是在一个数据管理类里使用 MMKV 的属性委托。比如写在 Model 类或者 Repository 类,保存和读取操作都是使用同一个属性,这就不会出错,并且能更容易追根溯源。

object DataRepository : MMKVOwner 
  var isDarkMode: Boolean by MMKVBoolProperty()


// 读取数据
if (DataRepository.isDarkMode) 
  ...


// 缓存数据
DataRepository.isDarkMode = true

使用高阶函数能更好地复用委托类,下面是个人封装好的代码,把 MMKV 支持的 9 种数据类型都封装了,代码会有点多。

interface MMKVOwner 
  val kv: MMKV get() = com.dylanc.mmkv.kv


val kv: MMKV = MMKV.defaultMMKV()

fun MMKVOwner.mmkvInt(default: Int = 0) =
  MMKVProperty(MMKV::decodeInt, MMKV::encode, default)

fun MMKVOwner.mmkvLong(default: Long = 0L) =
  MMKVProperty(MMKV::decodeLong, MMKV::encode, default)

fun MMKVOwner.mmkvBool(default: Boolean = false) =
  MMKVProperty(MMKV::decodeBool, MMKV::encode, default)

fun MMKVOwner.mmkvFloat(default: Float = 0f) =
  MMKVProperty(MMKV::decodeFloat, MMKV::encode, default)

fun MMKVOwner.mmkvDouble(default: Double = 0.0) =
  MMKVProperty(MMKV::decodeDouble, MMKV::encode, default)

fun MMKVOwner.mmkvString() =
  MMKVNullableProperty(MMKV::decodeString, MMKV::encode)

fun MMKVOwner.mmkvString(default: String) =
  MMKVNullablePropertyWithDefault(MMKV::decodeString, MMKV::encode, default)

fun MMKVOwner.mmkvStringSet(): ReadWriteProperty<MMKVOwner, Set<String>?> =
  MMKVNullableProperty(MMKV::decodeStringSet, MMKV::encode)

fun MMKVOwner.mmkvStringSet(default: Set<String>) =
  MMKVNullablePropertyWithDefault(MMKV::decodeStringSet, MMKV::encode, default)

fun MMKVOwner.mmkvBytes() =
  MMKVNullableProperty(MMKV::decodeBytes, MMKV::encode)

fun MMKVOwner.mmkvBytes(default: ByteArray) =
  MMKVNullablePropertyWithDefault(MMKV::decodeBytes, MMKV::encode, default)

inline fun <reified T : Parcelable> MMKVOwner.mmkvParcelable() =
  MMKVParcelableProperty(T::class.java)

inline fun <reified T : Parcelable> MMKVOwner.mmkvParcelable(default: T) =
  MMKVParcelablePropertyWithDefault(T::class.java, default)

class MMKVProperty<V>(
  private val decode: MMKV.(String, V) -> V,
  private val encode: MMKV.(String, V) -> Boolean,
  private val defaultValue: V
) : ReadWriteProperty<MMKVOwner, V> 
  override fun getValue(thisRef: MMKVOwner, property: KProperty<*>): V =
    thisRef.kv.decode(property.name, defaultValue)

  override fun setValue(thisRef: MMKVOwner, property: KProperty<*>, value: V) 
    thisRef.kv.encode(property.name, value)
  


class MMKVNullableProperty<V>(
  private val decode: MMKV.(String, V?) -> V?,
  private val encode: MMKV.(String, V?) -> Boolean
) : ReadWriteProperty<MMKVOwner, V?> 
  override fun getValue(thisRef: MMKVOwner, property: KProperty<*>): V? =
    thisRef.kv.decode(property.name, null)

  override fun setValue(thisRef: MMKVOwner, property: KProperty<*>, value: V?) 
    thisRef.kv.encode(property.name, value)
  


class MMKVNullablePropertyWithDefault<V>(
  private val decode: MMKV.(String, V?) -> V?,
  private val encode: MMKV.(String, V?) -> Boolean,
  private val defaultValue: V
) : ReadWriteProperty<MMKVOwner, V> 
  override fun getValue(thisRef: MMKVOwner, property: KProperty<*>): V =
    thisRef.kv.decode(property.name, null) ?: defaultValue

  override fun setValue(thisRef: MMKVOwner, property: KProperty<*>, value: V) 
    thisRef.kv.encode(property.name, value)
  


class MMKVParce

kotlin委托必须重视的几个点(代码片段)

委托模式是实现继承的一个很好的替代方式,也是Kotlin语言的一种特性,可以很优雅的实现委托模式。在开发过程中也少不了使用它,但常常都会被低估。所以今天就让它得到重视,去充分的掌握kotlin委托特性以... 查看详情

kotlin委托(代码片段)

1、类委托类的委托是一个类中定义的方法实际是调用另一个类的对象的方法来实现的。interfaceIDemofunaction1()funaction2()//被委托的类classDemoImpl:IDemooverridefunaction1()println("doaction1")overridefunaction2()println("doaction2")/ 查看详情

kotlin委托(代码片段)

1、类委托类的委托是一个类中定义的方法实际是调用另一个类的对象的方法来实现的。interfaceIDemofunaction1()funaction2()//被委托的类classDemoImpl:IDemooverridefunaction1()println("doaction1")overridefunaction2()println("doaction2")/ 查看详情

kotlin委托(代码片段)

1、类委托类的委托是一个类中定义的方法实际是调用另一个类的对象的方法来实现的。interfaceIDemofunaction1()funaction2()//被委托的类classDemoImpl:IDemooverridefunaction1()println("doaction1")overridefunaction2()println("doaction2")/ 查看详情

kotlin委托必须重视的几个点(代码片段)

委托模式是实现继承的一个很好的替代方式,也是Kotlin语言的一种特性,可以很优雅的实现委托模式。在开发过程中也少不了使用它,但常常都会被低估。所以今天就让它得到重视,去充分的掌握kotlin委托特性以... 查看详情

databinding——使用kotlin委托优化(代码片段)

简介DataBinding是Google在Jetpack中推出的一款数据绑定的支持库,利用该库可以实现在页面组件中直接绑定应用程序的数据源。使其维护起来更加方便,架构更明确简洁。启用DataBindingDataBinding库与AndroidGradle插件捆绑在一起。... 查看详情

kotlin学习之委托机制(代码片段)

类的委托    委托模式可以很好的替代实现继承,kotlin本身支持需要零样板代码,一个类Derived可以继承Base并委托它所有的public方法到一个指定的类:interfaceBasefunprint()classBaseImpl(valx:Int):Baseoverridefunprint()print(x)classDerived(b:B... 查看详情

深入kotlin-委托(代码片段)

委托委托不是java中的概念。它仅仅是一种设计模式,但是kotlin从语言层级上,通过by关键字提供了对委托模式的支持。类委托interfaceMyInteface//1 funmyPrint()classMyInterfaceImpl(valname:String):MyInterface//2 overridefunmyPring() println(name) 查看详情

kotlin中委托的概念和原理(代码片段)

kotlin中委托的概念和原理问题背景kotlin的日常使用过程中,经常会使用到委托机制,问题来了,委托机制究竟是什么呢?委托模式:多个对象接收并处理同一请求,他们将请求委托给另一个对象统一处理请求。比如调用A类的metho... 查看详情

kotlin学习记录(代码片段)

关于kotlin中的委托:1.属性委托  我们知道kotlin为我们提供了几种标准的委托,如下所示  延迟属性(lazyproperties):其值只在?次访问时计算;  可观察属性(observableproperties):监听器会收到有关此属性变更的通知;  把... 查看详情

微信开源库mmkv遍历读取存储的所有key以及对应的value方法(代码片段)

微信开源库MMKV遍历读取存储的所有key以及对应的value方法最近正在使用微信的开源库MMKV,替代项目中已存在的sharePreferences,替换过程非常简单,使用MMKV的导入SP接口直接把SP里面的数据全部转移到MMKV中。项目中存在... 查看详情

微信开源库mmkv遍历读取存储的所有key以及对应的value方法(代码片段)

微信开源库MMKV遍历读取存储的所有key以及对应的value方法最近正在使用微信的开源库MMKV,替代项目中已存在的sharePreferences,替换过程非常简单,使用MMKV的导入SP接口直接把SP里面的数据全部转移到MMKV中。项目中存在... 查看详情

kotlin委托工厂map存储属性值(代码片段)

1.委托工厂(1)概念?委托工厂顾名思义:生产委托对象的工厂类。(2)定义?该类实现了operator修饰的provideDelegate方法,返回ReadWriteProperty/ReadOnlyProperty,该类就可提供对应类型的委托对象。/***委托工厂类(生产委托对象)*/classP... 查看详情

kotlin小知识之泛型和委托(代码片段)

...属性实现一个自己的lazy函数泛型和委托泛型的基本用法Kotlin当中的泛型机制和Java当中的泛型机制还是有异同的所谓泛型就是说在一般的编程模式下面,我们需要给一个变量指定一个具体的类型,而泛型允许我们在不指定具体类型... 查看详情

对比java学kotlin代理(代码片段)

...c;而且使用Java无法完成对属性的委托。而我们今天的主角Kotlin则对上述问题做了改进。Kotlin代理在Kotlin中,我们使用关键字by使用委托功能。代理方法Kotlin对方法的委托是借助接口来实现的。具体用法如下:interfaceBasefunpr... 查看详情

kotlin对象枚举委托(代码片段)

目录一、Kotlin对象1.kotlin伴生对象2.kotlin对象和单例模式二、Kotlin枚举1.kotlin定义枚举2.kotlin使用枚举三、Kotlin委托1.kotlin类委托2.kotlin属性委托3.kotlinMap委托4.kotlin延迟属性5.kotlin属性监听附Github源码一、Kotlin对象1.kotlin伴生对象clas... 查看详情

kotlin实战之委托总结(代码片段)

...是觉得将自己的云笔记分享出来吧~特别说明,kotlin系列文章均以Java差异为核心进行提炼,与Java相同部分不再列出。随着kotlin官方版本的迭代,文中有些语法可能会发生变化,请务必留意,语言领悟精髓... 查看详情

再见mmkv,自己撸一个fastkv,真的很快(代码片段)

...MMKV,写入速度比前者高不少。后来官方又推出了基于Kotlin的DataStore,测试下来 查看详情