preface

Before sharing how to better use Kotlin syntax sugar to write tool class, and share personal polishing for nearly a year library, probably the most useful Kotlin tool class library, interested partners can know.

Some friends may find that how even a SharedPreferences so basic things are not encapsulated? I actually had a package and polished several versions, optimizing the code and usage to my satisfaction. But since SharedPreferences are no longer officially recommended, even if I keep them now, they will be deprecated later, so the code for the utility class has been removed from the official version.

So what do you use without the SharedPreferences utility class? You can choose to use the official recommended DataStore, which is the official alternative to using SharedPreferences. However, DataStore’s API is more copied than SharedPreferences, and Kotlin coroutine Flow is also used, which is relatively expensive to learn. So I recommend Tencent open source MMKV.

The use of MMKV is simple enough, but it is more useful when combined with the Kotlin attribute delegate. For those of you who are not familiar with Kotlin’s property delegate, this article explains exactly what a property delegate is and how it is implemented. Next, we will explain the nature of Kotlin delegation and MMKV packaging ideas.

The nature of a Kotlin delegate

What is a delegate

Before WE talk about Kotlin’s delegation, let’s talk about the delegation pattern. Delegation mode, also called agent mode, is a common design mode.

The entrusting mode is similar to the agent, purchasing agent and intermediary in our life. Some things are difficult for us to buy directly or do not know how to buy, but we can buy indirectly through agents, purchasing agents, intermediaries and other ways, so that we also have the ability to buy the things.

So how does that code work? First we define an interface and declare a purchase method:

interface Subject {
  fun buy(a)
}
Copy the code

Then write an agent class to implement the purchase function:

class Delegate : Subject {
  override fun buy(a) {
    print("I can buy it because I live abroad.")}}Copy the code

When a class needs the functionality but cannot implement it directly, it can implement it indirectly through a proxy.

class RealSubject : Subject {
    
  private val delegate: Subject = Delegate()
  
  override fun buy(a) {
    delegate.buy()
  }
} 
Copy the code

To summarize, the delegate (proxy) pattern is to hand off the interface function to another interface instance object. So the delegate pattern has template code, and each interface method calls the corresponding proxy object method.

There is template code, but Java has no good way of generating template code, and Kotlin can support it natively with zero template code. Delegating by keyword, we can change the above code to:

class RealSubject : Subject by Delegate(a)Copy the code

This delegate code will eventually generate the code above that creates the proxy object, in short the compiler generates the template code for us.

In addition, the expression after the by keyword can be written in many ways:

class RealSubject(delegate: Subject) : Subject by delegate

class RealSubject : Subject by globalDelegate

class RealSubject : Subject by GlobalDelegate

class RealSubject : Subject by delegate {... }
Copy the code

There are many ways to write this, but remember that the expression after BY must be an instance object of an interface. Because interface functionality is delegated to a concrete instance object, this object may be obtained through constructors, top-level attributes, singletons, methods, and so on.

If you don’t understand this, you’ll be confused by all the different ways you can write by. It’s not really Kotlin syntax, it’s just to get an object.

What is a property delegate

Interfaces delegate interface methods, so what do properties delegate? It’s also easy to imagine that properties can delegate get and set methods.

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

Of course, attribute delegate classes cannot be written arbitrarily, and there are specific rules. Let’s look at how to delegate get and set methods. Let’s look at an example of a delegate class:

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.")}Copy the code

The delegate class implements the attribute delegate:

var message: String by Delegate()
Copy the code

Now, some of you might be confused, why do you write delegate classes like that?

In fact, there is a set of fixed templates, but there is no need to memorize, because the official provides interface classes for us to quickly implement.

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)
}
Copy the code

But this interface is not necessary, we can also manually type the corresponding method to delegate properties, and I’m going to go through every point of the method.

Let’s first look at the method’s first argument, thisRef, which, as the name suggests, is a reference to this. Because the attribute might be in a class, it might need to call that class’s methods, and if you can’t even get the this reference, there’s no way to delegate. However, we may not need the outer class, which can be defined as Any? Type.

Then there is the second parameter, property, which must be of type KProperty<*> or its supertype. This parameter retrieves information about the property, such as its name. Why do we need this parameter? Because different businesses may be implemented based on different attributes. For example, if we are to do real estate intermediary, we cannot recommend the same house to everyone, the rich may buy a villa. We will feedback different results according to different customer information, which needs to get customer information. Similarly, a delegate class for an attribute needs to be able to get information about the attribute.

Most importantly, you need getValue() and setValue() methods with the operator keyword. This brings us to another advanced use of Kotlin — the overloaded operator. Let’s start with a scenario where we can see how overloaded operators work. For example, if we write a class, we can’t add two objects in a + b the way an Int does. But overloaded operators do this for us by adding the plus() method, which is decorated with the operator keyword. You can also overload a number of operators, such as a++, a > b, etc., you are interested in learning.

The names of methods that can be overloaded are fixed, such as the plus() method for addition. The overloaded getValue() and setValue() methods are the corresponding get and set methods of this class. Attribute delegate is to delegate the get and set methods of an attribute to the get and set methods of a proxy class.

All of these are necessary conditions for a property delegate, which you may not use, but you can’t do without.

The Kotlin library also provides several standard delegates that can be used to quickly implement property delegates in common scenarios.

Delay to entrust

Scenarios for delayed initialization. Typically, the delegate class code for delayed delegates is fixed, so a lazy() method is officially provided to simplify using the code.

val loadingDialog by lazy { LoadingDialog(this)}Copy the code

The first call to get the value of a property executes the result of a lambda expression, and subsequent calls to get the property are directly cached. In fact, I did the following.

private var _loadingDialog: LoadingDialog? = null
val loadingDialog: LoadingDialog
  get() {
    if (_loadingDialog == null) {
      _loadingDialog = LoadingDialog(this)}return _loadingDialog!!
  }
Copy the code

The lazy() method returns an object of the lazy class, so the compiler generates a delegate class of lazy instead of the previous ReadWriteProperty.

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

Observable delegation

It is easy to implement the observer mode. The delegate class code is also fixed, so Delegates officially provide the Delegates.Observable () method. Each time the value of the property is set, a callback will be received in the lambda expression.

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

This method returns an ObservableProperty object, inherited from ReadWriteProperty. The internal implementation is simple: the callback method is executed at setValue().

Attribute values for delegate mappings

Simply put, you delegate a property to the map.

class User(valmap: Map<String, Any? >) {val name: String by map
}
Copy the code

To obtain the above attributes, the map is used to obtain the value of the key name, and the compiler generates the following logic.

class User(valmap: Map<String, Any? >) {val name: String 
    get() = map["name"] as String
}
Copy the code

summary

The Kotlin delegate is basically the code that the compiler generates for us.

If it is an interface delegate:

class RealSubject : Subject by Delegate(a)Copy the code

The compiler generates delegate code for us:

class RealSubject : Subject {
    
  private val delegate: Subject = Delegate()
  
  override fun buy(a) {
    delegate.buy()
  }
} 
Copy the code

If it is a delegate for an attribute:

var name: String by PropertyDelegate()
Copy the code

The compiler generates logical code like the following:

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

The expression after the by keyword can be written in various ways, but it always returns a delegate object.

MMKV package idea

With Kotlin’s delegation replenishing, let’s wrap MMKV into practice.

Management MMKV

Usually we get the default MMKV object for encoding and decoding:

val kv = MMKV.defaultMMKV()
kv.encode("bool".true)
val bValue = kv.decodeBool("bool")
Copy the code

But sometimes there are other situations where different businesses need to store differently:

val kv = MMKV.mmkvWithID("MyID")
Copy the code

MULTI_PROCESS_MODE: mmkv.multi_process_mode:

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

So instead of writing the default MMKV, we need to provide a way to switch the MMKV object. This allows you to use the interface. We’ll refer to the official LifecycleOwner to write an MMKVOwner interface that gets a KV attribute. DefaultMMKV () is the default.

interface MMKVOwner {
  val kv: MMKV get() = defaultMMKV

  companion object {
    private val defaultMMKV = MMKV.defaultMMKV()
  }
}
Copy the code

Once the interface is implemented, you can quickly use MMKV in your class.

object UserRepository : MMKVOwner {
  fun saveUser(user: User) {
  	kv.encode("user", user)
  }
    
  fun logout(a) {
    kv.clearAll()
  }
}
Copy the code

If you need to store it differently, override the KV property.

object UserRepository : MMKVOwner {
  override val kv: MMKV = MMKV.mmkvWithID("user")}Copy the code

In my opinion, this interface package is superior to the following common MMKV tool class package.

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")
Copy the code

The utility class is simply designed to eliminate the need to create an MMKV object, which requires a lot of code. Moreover, the default MMKV singleton is not flexible enough to be switched uniformly.

Using attribute delegate

Now that the MMKVOwner interface can use all the functionality of MMKV, attribute delegates are used to simplify common access operations.

Since we are using attribute delegate, we can use the name of the attribute as the key value, leaving out one parameter. As we try to operate on the same property, my personal recommendation is to limit the use of property delegates in MMKVOwner’s implementation class.

ReadWriteProperty

, getValue() and setValue() call MMKV encoding and decoding methods, The key value uses 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)
  }
}
Copy the code

This allows us to delegate a Boolean property to MMKV.

object DataRepository : MMKVOwner {
  var isDarkMode: Boolean by MMKVBoolProperty()
}
Copy the code

You could have changed MMKVOwner to Any? If thisRef is not of MMKVOwner type, use mmkv.defaultmmkv () so it can work in any class. I don’t recommend it because it’s so convenient to write property delegates in two separate places. For example, the home page will read cache whether to enable night mode, modify this configuration in the Settings.

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
  }
}
Copy the code

In case the property name is incorrectly typed, the data is abnormal. If you really want to use it separately, go back to encode() and decode(), using a constant as the key to keep the data consistent.

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)
  }
}
Copy the code

Of course, it is recommended that MMKVOwner be implemented in either the Model or Repository classes, and access operations use the same property, so that there is no problem.

We can also continue to optimize the delegate class code and extract a common delegate class for reuse so that other delegate methods can be implemented quickly.

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)
  }
}
Copy the code

Final plan

Since MMKV supports nine types of data, it requires a lot of code to encapsulate it.

So I have written a library mmKV-KTX for everyone to use.

Begin to use

To build. Gradle in the root directory add:

allprojects {
    repositories {
        / /...
        maven { url 'https://www.jitpack.io'}}}Copy the code

Add a dependency to the build.gradle module:

dependencies {
    implementation 'com. Making. DylanCaiCoding: MMKV - KTX: 1.2.11'
}
Copy the code

To make a class implement the MMKVOwner interface, you can delegate properties to MMKV by using the mmkvXXXX() method, for example:

object DataRepository : MMKVOwner {
  var isFirstLaunch by mmkvBool(default = true)
  var user by mmkvParcelable<User>()
}
Copy the code

The corresponding encode or decode method is called when setting or retrieving the value of an attribute, and the key value is the attribute name.

The following types are supported:

methods The default value
mmkvInt() 0
mmkvLong() 0L
mmkvBool() false
mmkvFloat() 0f
mmkvDouble() 0.0
mmkvString() /
mmkvStringSet() /
mmkvBytes() /
mmkvParcelable() /

The implementation class of MMKVOwner can obtain KV objects for deleting values or clearing the cache:

kv.removeValueForKey(::isFirstLaunch.name)
kv.clearAll()
Copy the code

If different services need different storage, you can rewrite the KV property to create different MMKV instances:

object DataRepository : MMKVOwner {
  override val kv: MMKV = MMKV.mmkvWithID("MyID")}Copy the code

See the unit test code for the full usage.

conclusion

This article goes into detail about the use and nature of the Kotlin delegate, which the compiler generates for us. Then the MMKV encapsulation idea of interface + attribute delegation is shared. Finally, I shared my good open source library MMKV-KTX, which is convenient for our daily development and use.

If you find it helpful, please click star to support it. I will share more articles related to encapsulation with you later.

Future articles on encapsulation

  • How to Better Use the Kotlin Syntax Sugar Wrapper Utility Class
  • Elegantly encapsulating and using ViewBinding, time to replace Kotlin Synthetic and ButterKnife
  • ViewBinding cleverly encapsulates ideas and ADAPTS BRVAH in this way
  • Gracefully Handling Data Returned from the background