优雅地封装 MMKV,属性委托原来是这么回事

Posted 涂程

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了优雅地封装 MMKV,属性委托原来是这么回事相关的知识,希望对你有一定的参考价值。

好文推荐
作者:DylanCai

前言

可能有的小伙伴发现,怎么连个 SharedPreferences 这么基础的东西都没有封装?其实我有封装,并且打磨了几个版本,代码和用法都优化得自己觉得满意。不过由于官方已经不推荐用 SharedPreferences,即使我现在保留了,后续也会标记为弃用,所以正式版移除了该工具类的代码。

那么没 SharedPreferences 工具类用什么呢?可以选择使用官方推荐的 DataStore,这是官方弃用 SharedPreferences 后给的替代方案。不过 DataStore 的 API 比 SharedPreferences 复杂得多,还用到了 Kotlin 协程的 Flow,学习成本比较高。所以个人推荐用腾讯开源的 MMKV

MMKV 的用法其实已经挺简单了,不过结合 Kotlin 属性委托会更加好用。应该有很多人对 Kotlin 的属性委托不熟悉,所以本文会讲清楚属性委托到底是什么东西,怎么实现的属性委托。接下来为大家讲解 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)

当然属性的委托类不能随便写,有一套具体的规则。我们先讲一下怎么把 get、set 方法委托出去,先来看一个委托类的示例:

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.")

有了这个委托类就能实现属性委托:

var message: String by Delegate()

可能有些人看了就懵了,委托类为什么要这么写?

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

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++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

小结

Kotlin 委托其实就是编译器帮我们生成了委托类的代码。

如果是接口的委托:

class RealSubject : Subject by Delegate()

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

class RealSubject : Subject 

  private val delegate: Subject = Delegate()

  override fun buy() 
    delegate.buy()
  
 

如果是属性的委托:

var name: String by PropertyDelegate()

编译器就会帮我们生成类似下面的逻辑代码:

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

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

MMKV 封装思路

补充完 Kotlin 委托的知识,我们马上来封装 MMKV 实践一下。

管理 MMKV

通常我们是获取默认的 MMKV 对象进行编码和解码:

val kv = MMKV.defaultMMKV()
kv.encode("bool", true)
val bValue = kv.decodeBool("bool")
复制代码

但是有时会有其它情况,比如不同业务需要区别存储:

val kv = MMKV.mmkvWithID("MyID")

又或者业务需要多进程访问,可以在初始化的时候加上标志位 MMKV.MULTI_PROCESS_MODE

val kv = MMKV.mmkvWithID("InterProcessKV", MMKV.MULTI_PROCESS_MODE)

所以我们不能写死默认的 MMKV,需要提供一种方式可以切换 MMKV 对象。这就可以使用到接口,我们参考官方的 LifecycleOwner 写一个 MMKVOwner 接口,该接口能获取一个 kv 属性,默认获取 MMKV.defaultMMKV()

interface MMKVOwner 
  val kv: MMKV get() = defaultMMKV

  companion object 
    private val defaultMMKV = MMKV.defaultMMKV()
  

实现接口后,就能在类里快速使用 MMKV。

object UserRepository : MMKVOwner 
  fun saveUser(user: User) 
  	kv.encode("user", user)
  

  fun logout() 
    kv.clearAll()
  

如果需要区别存储,重写 kv 属性即可。

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

个人认为这个接口封装是优于下面常见的 MMKV 工具类封装。

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)
  

  // ...


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

这个工具类的作用只是为了省去创建 MMKV 对象的步骤,需要写很多代码。而且写死了用默认的 MMKV 单例,不够灵活,不能统一切换。

使用属性委托

MMKVOwner 接口已经能使用 MMKV 的所有功能,接下来用属性委托简化常见的存取操作。

既然我们用属性委托,那就可以使用属性的名字作为 key 值,省去一个参数。这样的话我们要尽量操作同一个属性,所以个人建议是限制属性委托在 MMKVOwner 的实现类里使用。

下面封装一个 Boolean 值的属性委托,实现 ReadWriteProperty<MMKVOwner, Boolean>,在 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)
  

这样我们就能把一个 Boolean 类型的属性委托给 MMKV。

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

其实可以把上面的委托类的 MMKVOwner 改成 Any?,判断 thisRef 不是 MMKVOwner 类型的话,使用 MMKV.defaultMMKV() ,这样就能在任何类里都能用。但个人并不推荐,因为太方便了,很容易分开两处地方来写属性委托。比如在首页会读取缓存是否需要开启夜间模式,在设置里修改该配置。

class MainActivity : AppCompatActivity() 
  private val isDarkMode by MMKVBoolProperty()

  override fun onCreate(savedInstanceState: Bundle?) 
    // ...
    if (isDarkMode) 
      setDarkModel()
    
  


class SettingActivity : AppCompatActivity() 
  private var isDarkMode by MMKVBoolProperty()

  private fun onCheckedChanged(isChecked: Boolean) 
    // ...
    isDarkModel = isChecked
  

万一属性名敲错了,数据就异常了。真想分开用的话还是用回原来的 encode()decode() 方法吧,使用一个常量作为 key 值保证数据的一致性。

const val KEY_DARK_MODE = "dark_mode"

class MainActivity : AppCompatActivity(), MMKVOwner 

  override fun onCreate(savedInstanceState: Bundle?) 
    // ...
    if (kv.decodeBool(KEY_DARK_MODE)) 
      setDarkModel()
    
  


class SettingActivity : AppCompatActivity(), MMKVOwner 

  private fun onChecked(isChecked: Boolean) 
    // ...
    kv.encode(KEY_DARK_MODE, isChecked)
  

当然推荐的用法是,在 Model 类或 Repository 类实现 MMKVOwner ,存取操作都是使用同一个属性,这样就不会有问题。

我们还能继续优化委托类的代码,抽取一个公用的委托类进行复用,这样就能快速实现其它委托方法了。

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

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

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

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

fun MMKVOwner.mmkvDouble(default: Double = 0.0) =
  MMKVProperty(this, MMKV::decodeDouble, MMKV::encode, 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)
  

最终方案

由于 MMKV 支持 9 种类型的数据,自己封装的话需要写不少代码。

所以个人已经写好了一个库 MMKV-KTX 给大家使用。

开始使用

在根目录的 build.gradle 添加:

allprojects 
    repositories 
        //...
        maven  url 'https://www.jitpack.io' 
    

在模块的 build.gradle 添加依赖:

dependencies 
    implementation 'com.github.DylanCaiCoding:MMKV-KTX:1.2.11'

让一个类实现 MMKVOwner 接口,即可通过 by mmkvXXXX() 方法将属性委托给 MMKV,例如:

object DataRepository : MMKVOwner 
  var isFirstLaunch by mmkvBool(default = true)
  var user by mmkvParcelable<User>()

设置或获取属性的值则调用对应的 encode 或 decode 方法,key 值为属性名。

支持以下类型:

方法默认值
mmkvInt()0
mmkvLong()0L
mmkvBool()false
mmkvFloat()0f
mmkvDouble()0.0
mmkvString()/
mmkvStringSet()/
mmkvBytes()/
mmkvParcelable()/

MMKVOwner 的实现类可以获取 kv 对象进行删除值或清理缓存等操作:

kv.removeValueForKey(::isFirstLaunch.name)
kv.clearAll()

如果不同业务需要区别存储,可以重写 kv 属性来创建不同的 MMKV 实例:

object DataRepository : MMKVOwner 
  override val kv: MMKV = MMKV.mmkvWithID("MyID")

完整的用法可查看单元测试代码。

总结

本文详细地讲解了 Kotlin 委托的用法和本质,其实就是编译器帮我们生成了委托的代码。然后分享了接口 + 属性委托的 MMKV 封装思路。最后分享了个人实现好的开源库 MMKV-KTX,方便大家日常开发使用。

如果您觉得有帮助的话,希望能点个 star 支持一下哟 ~ 我后面会分享更多封装相关的文章给大家。

以上是关于优雅地封装 MMKV,属性委托原来是这么回事的主要内容,如果未能解决你的问题,请参考以下文章

Kotlin 委托的本质以及 MMKV 的应用

《关于实现一个函数把真实dom转换成虚拟dom原来是这么一回事》

变分自编码器:原来是这么一回事

从底层分析Spring源码,原来Spring是这么回事

原来以为(Trie)字典树很难,后来发现不久这么一回事嘛

原来以为(Trie)字典树很难,后来发现不久这么一回事嘛