preface

This paper mainly includes the following contents: 1. What are the defects of MMKV? 2. Practical tips for optimizing MMKV calls using Kotlin delegate

If you find this article helpful, please give it a thumbs up. Thank you

MMKVWhat are the defects

As we have mentioned before, MMKV has many advantages compared with SharedPreferences. For example, mmap prevents data loss and improves read and write efficiency. 2. Reduce data size by minimizing the amount of data to represent the most information. 3. Incremental update to avoid full write of a large amount of data. MMKV integration and principle SharedPreferences

So what are the disadvantages of MMKV? The main disadvantage of MMKV is that it does not support getAll

publicMap<String, ? > getAll() {throw new UnsupportedOperationException("use allKeys() instead, getAll() not implement because type-erasure inside mmkv");
}
Copy the code

MMKV is stored in bytes, the actual writing of files erases the type, which is why MMKV does not support getAll

Although getAll is not used much, MMKV does not have the ability to export and migrate. For example, when a better storage framework is developed, for example, DataStore is officially released, there is no way to directly migrate from MMKV to the new framework in batches, unless the code has written a number of key migrations, which is very troublesome, so when we introduce MMKV, we should take into account the possible data migration in the future

How to makeMMKVsupportgetAll?

Since the reason MMKV doesn’t support getAll is because the type is erased, the easiest way to think about it is to add a type suffix to the key

A more elegant approach would be to add a proxy layer to which all read and write operations are delegated and where the key is typed

class SpProxy(private val mmkv: MMKV?) : SharedPreferences, SharedPreferences.Editor {
    override fun getAll(a): MutableMap<String, *> {
        valkeys = mmkv? .allKeys()valmap = mutableMapOf<String, Any>() keys? .forEach {if (it.contains("@")) {
                val typeList = it.split("@")
                when (typeList[typeList.size - 1]) {
                    String::class.simpleName -> map[it] = getString(it, "") ?: ""
                    Int: :class.simpleName -> map[it] = getInt(it, 0)
                    Long: :class.simpleName -> map[it] = getLong(it, 0L)
                    Float: :class.simpleName -> map[it] = getFloat(it, 0f)
                    Boolean: :class.simpleName -> map[it] = getBoolean(it, false)}}}return map
    }

    override fun getString(key: String? , defValue:String?).: String? {
        val typeKey = getTypeKey<String>(key)
        returnmmkv? .getString(typeKey, defValue) }override fun getBoolean(key: String? , defValue:Boolean): Boolean {
        val typeKey = getTypeKey<Boolean>(key)
        returnmmkv? .getBoolean(typeKey, defValue) ? : defValue } ...override fun contains(key: String?).: Boolean {
        val realKey = getRealKey(key)
        return realKey.isNotEmpty()
    }

    override fun edit(a): SharedPreferences.Editor? {
        returnmmkv? .edit() }override fun putString(key: String? , value:String?).: SharedPreferences.Editor? {
        val typeKey = getTypeKey<String>(key)
        returnmmkv? .putString(typeKey, value) }override fun putBoolean(key: String? , value:Boolean): SharedPreferences.Editor? {
        val typeKey = getTypeKey<Boolean>(key)
        returnmmkv? .putBoolean(typeKey, value) }override fun remove(key: String?).: SharedPreferences.Editor? {
        val realKey = getRealKey(key)
        if (realKey.isNotEmpty()){
            returnmmkv? .remove(realKey) }return null
    }

    override fun clear(a): SharedPreferences.Editor? {
        returnmmkv? .clear() }inline fun <reified T> getTypeKey(key: String?).: String {
        val type = "@" + T::class.simpleName
        return if(key? .contains(type) ==true) {
            type
        } else {
            key + type
        }
    }

    private fun getRealKey(key: String?).:String{
        val typeKys = listOf(getTypeKey<String>(key),getTypeKey<Long>(key),getTypeKey<Float>(key),getTypeKey<Int>(key),getTypeKey<Boolean>(key))
        typeKys.forEach {
            if(mmkv? .containsKey(it)==true) {return it
            }
        }
        return ""}}Copy the code

All read and write operations are performed based on SpProxy. 2. The getTypeKey method is used to implement the getTypeKey method 3. Support getAll method, convenient for subsequent migration 4. MMKV operations are all encapsulated in SpProxy class, if you want to migrate to other classes, you can modify SpProxy, external can not modify at all

All problems in computer software can be solved by adding an intermediate layer, encapsulating an agent layer for SharedPreferences, which can effectively extend the extensibility of our project

How do I migrate old data

MMKV importFromSharedPreferences method was provided for us to migrated from SharedPreferences MMKV but if invoke this method directly transfer, then the data type is missing, here to provide a new method of migration

fun migrate(migrateSp:SpProxy,preferences: SharedPreferences){
    val kvs = preferences.all
   if(kvs ! =null && kvs.isNotEmpty()) {
       val iterator = kvs.entries.iterator()
        while (iterator.hasNext()) {
            val entry = iterator.next()
            val key = entry.key
            val value = entry.value
            if(key ! =null&& value ! =null) {
                migrateSp.run {
                    when (value) {
                        is Boolean -> this.putBoolean(key, value)
                        is Int ->  this.putInt(key,value)
                        is Long -> this.putLong(key,value)
                        is Float -> this.putFloat(key, value)
                        is String -> this.putString(key,value)
                        else -> {}
                    }
                }
            }
        }
        kvs.size
    }
}
Copy the code

As shown above: Support for migrating data from SharedPreferences to MMKV and retaining types

Optimize access operations with delegates

We typically use SharedPreferences to access data like this

private val KEY_DEMO_STR = "key_demo_STR"
fun setDemoStr(str: String) {
    edit.putString(KEY_DEMO_STR, str).apply()
}

fun getDemoStr(a): String? {
    return preferences.getString(KEY_DEMO_STR, "")}Copy the code

First we need to define a key, then we need to define the set and get methods. Such a simple operation takes eight lines of code, but we can do it in one line using the delegate mechanism

Data access in one line

The optimized code implements data access in one line

object TestSP : PreferenceHolder() {
    var value: Long by bindToPreferenceField(0L)}/ / read sp
val value = TestSP.value
println(value) // 0 or 100
/ / in sp
TestSP.value = 100
Copy the code

As shown above, data can be accessed by reading and assigning the value of the variable. The principle behind this is to generate the key from the variable name, Then, the data access operation is entrusted to ReadWriteProperty, and the MMKV or SharedPreferences methods are called in getValue and setValue to realize the real storage and reading

The advantages of this approach are 1. Avoid defining a large number of string keys and duplicate keys 2. Concise delegate mode eliminates the need to write get(..) set(..) 3. If you need to modify the code later, you can modify the delegate class, and other codes in the project do not need to be changed, which enhances the scalability

Preferences Delegate optimization

conclusion

This paper mainly describes the defects of MMKV: that is, the subsequent migration difficulties caused by not supporting getAll and the solutions, and introduces the experience of optimizing data access API calls with delegation

Show Me The Code

This article code can be seen: SpProxy

The resources

SharedPreferences Kotlin should write this to talk about the shortcomings of MMKV and offline debugging tools