Several points that Kotlin entrustment must pay attention to

Posted by Anyaer on Mon, 24 Jan 2022 02:50:15 +0100

Delegation mode is a good alternative to inheritance. It is also a feature of kotlin language. It can implement delegation mode gracefully. It is also indispensable in the development process, but it is often underestimated. So let's pay attention to it today to fully grasp the characteristics and principles of kotlin delegation.

1, Delegate class

We first complete a delegate class, which is often used to implement the delegate mode of the class. Its key is to use the by keyword:

interface Base{
  fun print()
}

class BaseImpl(val x: Int): Base{
  override fun print() { print(x) }
}

class Derived(b: Base): Base by b

fun main(){
  val b = BaseImpl(10)
  Deriived(b).print()
}

//Finally output 10

In this delegation mode, Derived is equivalent to a wrapper. Although it also implements base, it doesn't care how it is implemented. Through the keyword by, it delegates the implementation of the interface to its parameter db.

Structure equivalent to Java code:

class Derived implements Base{
  Base b;
  public Derived(Base b){ this.b = b}
}

2, Delegate properties

As mentioned earlier, kotlin delegate classes delegate interface methods, and delegate properties delegate getter and setter methods of properties. Delegate attribute syntax supported by kotlin:

class Example {
    var prop: String by Delegate()
}

Property will be delegated to its getValue and setValue methods. Of course, the delegation of the attribute is not written casually. For the val attribute, it must provide a getValue function. If it is a var attribute, it must also provide a setValue attribute. Let's take a look at the official Delegate attribute:

class Delegate {
    operator fun getValue(thisRef: Any?, property: KProperty<*>): String {
        return "$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.")
    }
}

We can see that the attributes modified by var must have getValue and setValue methods, and these two methods must be modified by the operator keyword.

Let's look at the first parameter thisRef. Its type is the type of the property owner or its parent class. When we are not sure which class the attribute will belong to, we can define the type of thisRef as Any? Yes.

Then look at another parameter property. Its type must be kproperty < * > or its supertype, and its value is the name prop of the previous field.

The last parameter, whose type must be the type of the delegate property or its parent class. That is, the value: String in the example can also be replaced by value: Any.

Let's test whether this is true:

fun main() {
    println(Test().prop)
    Test().prop = "Hello, World"
}

You will see the output:

Example@5197848c, thank you for delegating 'prop' to me!
Hello, World has been assigned to 'prop' in Example@17f052a3.

2.1 user defined delegation

After knowing how to write delegate attributes, you can also implement your own attribute delegation according to your needs. However, it is troublesome to write so much template code every time, so the official also provides interface classes for us to implement quickly:

interface ReadOnlyProperty<in R, out T> {
    operator fun getValue(thisRef: R, property: KProperty<*>): T
}

interface ReadWriteProperty<in R, T> {
    operator fun getValue(thisRef: R, property: KProperty<*>): T
    operator fun setValue(thisRef: R, property: KProperty<*>, value: T)
}

Now the delegated class only needs to implement one of the interfaces. For the val variable, use ReadOnlyProperty, while the var variable implements ReadWriteProperty. Let's use the ReadWriteProperty interface to implement a custom delegate:

class Owner {
  var text: String by StringDelegate()
}


class StringDelegate(private var s: String = "Hello"): ReadWriteProperty<Owner, String> {
    override operator fun getValue(thisRef: Owner, property: KProperty<*>): String {
        return s
    }
    override operator fun setValue(thisRef: Owner, property: KProperty<*>, value: String) {
        s = value
    }
}

3, Entrusted advanced

3.1 lazy loading delegation

Lazy loading delegates, that is, when we operate on some resources, we hope it will be triggered when it is accessed to avoid unnecessary consumption. The official has provided us with a lazy() method to quickly create lazy load delegates:

val lazyData: String by lazy {
    request()
}

fun request(): String {
    println("Execute network request")
    return "network data "
}

fun main() {
    println("start")
    println(lazyData)
    println(lazyData)
}

//result:
start
 Execute network request
 network data 
network data 

You can see that only the first call will execute the logic in the lambda expression, and subsequent calls will only return the final result of the lambda expression.

So how is lazy loading delegate implemented? Now let's look at its source code:

public actual fun <T> lazy(initializer: () -> T): Lazy<T> = SynchronizedLazyImpl(initializer)

public actual fun <T> lazy(mode: LazyThreadSafetyMode, initializer: () -> T): Lazy<T> =
    when (mode) {
        LazyThreadSafetyMode.SYNCHRONIZED -> SynchronizedLazyImpl(initializer)
        LazyThreadSafetyMode.PUBLICATION -> SafePublicationLazyImpl(initializer)
        LazyThreadSafetyMode.NONE -> UnsafeLazyImpl(initializer)
    }

In this, the lazy() method will receive a parameter of LazyThreadSafetyMod type. If this parameter is not passed, the synchronized lazyimpl method will be used by default. You can see from the explanation that it is used for multi-threaded synchronization, while the other two are not multi-threaded safe.

  • LazyThreadSafetyMode.PUBLICATION: the initialization method can be called multiple times, but the value is only the return value of the first return, that is, only the first return value can be assigned to the initialization value.

  • LazyThreadSafetyMode. NONE: if initialization will always occur on the same thread as the property, it can be used in this case, but it has no synchronization lock.

Now let's focus on what is done in synchronized lazyimpl:

private class SynchronizedLazyImpl<out T>(initializer: () -> T, lock: Any? = null) : Lazy<T>, Serializable {
    private var initializer: (() -> T)? = initializer
    @Volatile private var _value: Any? = UNINITIALIZED_VALUE
    // final field is required to enable safe publication of constructed instance
    private val lock = lock ?: this

    override val value: T
        get() {
            val _v1 = _value
            //Judge whether it has been initialized. If it has been initialized, it will be returned directly without calling the internal logic of advanced functions
            //If the two values are different, it means that the current value has been loaded and returned directly
            if (_v1 !== UNINITIALIZED_VALUE) {
                @Suppress("UNCHECKED_CAST")
                return _v1 as T
            }

            return synchronized(lock) {
                val _v2 = _value
                if (_v2 !== UNINITIALIZED_VALUE) {
                    @Suppress("UNCHECKED_CAST") (_v2 as T)
                } else {
                  //Call the advanced function to get its return value
                    val typedValue = initializer!!()
                  //Assign the return value to_ Value, which is used to directly return the return value of the high-level function in the next judgment
                    _value = typedValue
                    initializer = null
                    typedValue
                }
            }
        }
			......
}

Through the above code, we can find that synchronized lazyimpl overrides the return value of the lazy interface and rewrites the accessor of the property. The specific logic is similar to Java double check. But how does the lazy interface become a delegate attribute?

In Lazy It is found in the KT file that it declares the getValue extension attribute of the Lazy interface, which will be called when the final assignment is made. As we said in the custom delegate, for the val attribute, we need to provide a getValue function.

## Lazy.kt
//This extension allows property delegation using an instance of Lazy
public inline operator fun <T> Lazy<T>.getValue(thisRef: Any?, property: KProperty<*>): T = value

With this lazy loading delegate, it will be easier for us to implement the singleton:

class SingletonDemo private constructor() {
    companion object {
        val instance: SingletonDemo by lazy{
        SingletonDemo() }
    }
}

3.2 Delegates.observable observer delegation

If you want to observe the change process of an attribute, you can delegate the attribute to delegates Observable, which has three parameters: assigned attribute, old value and new value:

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

An ObservableProperty object inherited from ReadWriteProperty is returned. Let's look at its internal implementation:

public inline fun <T> observable(initialValue: T, crossinline onChange: (property: KProperty<*>, oldValue: T, newValue: T) -> Unit):
            ReadWriteProperty<Any?, T> =
        object : ObservableProperty<T>(initialValue) {
            override fun afterChange(property: KProperty<*>, oldValue: T, newValue: T) = onChange(property, oldValue, newValue)
        }

initialValue is the initial value, and the other parameter onChange is the callback handler when the property value is modified.

3.3 by map mapping delegation

A common use case is to store attribute values in a map. It can use Map/MutableMap to implement attribute delegation:

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

fun main(args: Array<String>) {
    val map = mutableMapOf(
        "name" to "ha-ha"
    )
    val user = User(map)
    println(user.name)
    map["name"] = "LOL"
    println(user.name)
}

//Output:
ha-ha
LoL

However, there will be a problem during use. If there is no mapping value of delegate attribute name in the Map, an exception will be thrown when taking value: Key $key is missing in the map:

## MapAccessors.kt
public inline operator fun <V, V1 : V> MutableMap<in String, out @Exact V>.getValue(thisRef: Any?, property: KProperty<*>): V1 = (getOrImplicitDefault(property.name) as V1)

@kotlin.internal.InlineOnly
public inline operator fun <V> MutableMap<in String, in V>.setValue(thisRef: Any?, property: KProperty<*>, value: V) {
    this.put(property.name, value)
}

## MapWithDefault.kt
internal fun <K, V> Map<K, V>.getOrImplicitDefault(key: K): V {
    if (this is MapWithDefault)
        return this.getOrImplicitDefault(key)

    return getOrElseNullable(key, { throw NoSuchElementException("Key $key is missing in the map.") })
}

Therefore, when using, you must pay attention to the mapping value.

3.4 direct delegation between two attributes

Starting from Kotlin 1.4, we can directly delegate "attribute A" to "attribute B" at the syntax level, as shown in the following example:

class Item {
    var count: Int = 0
    var total: Int by ::count
}

The value of total in the above code is exactly the same as count, because we delegate the getter and setter of the total attribute to count. The following specific logic can be explained by code:

class Item {
    var count: Int = 0

    var total: Int
        get() = count

        set(value: Int) {
            count = value
        }
}

In writing, the delegate name can use the ":" qualifier, such as this::delegate or MyClass::delegate.

This usage is useful when the field is changed and the original field is retained. You can define a new field and delegate it to the original field, so you don't have to worry about the different values of the new and old fields.

3.5 provision of entrustment

What if you need to do some extra judgment before binding attribute delegates? We can define provideDelegate to implement:

class StringDelegate(private var s: String = "Hello") {                                                     
    operator fun getValue(thisRef: Owner, property: KProperty<*>): String {
        return s
    }                       
    operator fun setValue(thisRef: Owner, property: KProperty<*>, value: String) {
            s = value
    }
}


class SmartDelegator {

    operator fun provideDelegate(
        thisRef: Owner,
        prop: KProperty<*>
    ): ReadWriteProperty<Owner, String> {
	//Pass in different initial values according to the name of the property delegate
        return if (prop.name.contains("log")) {
            StringDelegate("log")
        } else {
            StringDelegate("normal")
        }
    }
}

class Owner {
    var normalText: String by SmartDelegator()
    var logText: String by SmartDelegator()
}

fun main() {
    val owner = Owner()
    println(owner.normalText)
    println(owner.logText)
}

//result:
normal
log

Here, we create a new SmartDelegator. We set another layer of the member method provideDelegate, and then make some logical judgments in it. Finally, we delegate the property to getStringDelegate.

This ability to intercept the binding between attributes and their delegates greatly shortens the logic that attribute names must be passed in order to achieve the same function.

4, Entrusted chestnut

4.1 simplify Fragment / Activity parameter transfer

It's annoying to write a large piece of code every time when transferring parameters to Fragment. Now with the magic weapon of delegation, we can simplify it together. The normal mode is as follows:

class BookDetailFragment : Fragment(R.layout.fragment_book_detail) {

    private var bookId: Int? = null
    private var bookType: Int? = null

    companion object {

        const val EXTRA_BOOK_ID = "bookId"
        const val EXTRA_BOOK_TYPE = "bookType";

        fun newInstance(bookId: Int, bookType: Int?) = BookDetailFragment().apply {
            Bundle().apply {
                putInt(EXTRA_BOOK_ID, bookId)
                if (null != bookType) {
                    putInt(EXTRA_BOOK_TYPE, bookType)
                }
            }.also {
                arguments = it
            }
        }
    }

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)

        arguments?.let {
            bookId = it.getInt(EXTRA_book_ID, 123)
            bookType = it.getInt(EXTRA_BOOK_TYPE, 1)
        }
    }
}

After writing such a long paragraph, we have finally written the basic method of parameter transfer. When obtaining the value, we have to deal with the case that the parameter is empty. Now we will extract the delegate class and re implement the above functions in the way of attribute delegation:

class BookDetailFragment : Fragment(R.layout.fragment_book_detail) {

    private var bookId: Int by argument()

    companion object {
        fun newInstance(bookId: Int, bookType: Int) = BookDetailFragment().apply {
            this.bookId = bookId
        }
    }

    override fun onViewCreated(root: View, savedInstanceState: Bundle?) {
      Log.d("tag", "BOOKID:" + bookId);
    }
}

It seems that a lot of code has been reduced. Isn't it amazing? The following implementation ideas are as follows:

class FragmentArgumentProperty<T> : ReadWriteProperty<Fragment, T> {

    override fun getValue(thisRef: Fragment, property: KProperty<*>): T {
      //Bunndle value should be processed separately
        return thisRef.arguments?.getValue(property.name) as? T
            ?: throw IllegalStateException("Property ${property.name} could not be read")
    }

    override fun setValue(thisRef: Fragment, property: KProperty<*>, value: T) {
        val arguments = thisRef.arguments ?: Bundle().also { thisRef.arguments = it }
        if (arguments.containsKey(property.name)) {
            // The Value is not expected to be modified
            return
        }
      	//The Bunndle setting shall be processed separately
        arguments[property.name] = value
    }
}

fun <T> Fragment.argument(defaultValue: T? = null) = FragmentArgumentProperty(defaultValue)

4.2 simplified SharedPreferences access value

Is it convenient if we can access values now

private var spResponse: String by PreferenceString(SP_KEY_RESPONSE, "")

// Read, show cache
display(spResponse)

// Update cache
spResponse = response

The answer is yes, or use delegate attributes to transform. The following is a specific implementation example:

class PreDelegate<T>(
        private val name: String,
        private val default: T,
        private val isCommit: Boolean = false,
        private val prefs: SharedPreferences = App.prefs) {

    operator fun getValue(thisRef: Any?, property: KProperty<*>): T {
        return getPref(name, default) ?: default
    }

    operator fun setValue(thisRef: Any?, property: KProperty<*>, value: T) {
        value?.let {
            putPref(name, value)
        }
    }

    private fun <T> getPref(name: String, default: T): T? = with(prefs) {
        val result: Any? = when (default) {
            is Long -> getLong(name, default)
            is String -> getString(name, default)
            is Int -> getInt(name, default)
            is Boolean -> getBoolean(name, default)
            is Float -> getFloat(name, default)
            else -> throw IllegalArgumentException("This type is not supported")
        }

        result as? T
    }

    private fun <T> putPref(name: String, value: T) = with(prefs.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 -> throw IllegalArgumentException("This type is not supported")
        }

        if (isCommit) {
            commit()
        } else {
            apply()
        }
    }
}

4.3 binding of data and View

With delegates, data and views can be bound without DataBinding.

operator fun TextView.provideDelegate(value: Any?, property: KProperty<*>) = object : ReadWriteProperty<Any?, String?> {
    override fun getValue(thisRef: Any?, property: KProperty<*>): String? = text
    override fun setValue(thisRef: Any?, property: KProperty<*>, value: String?) {
        text = value
    }
}

Write an extension function to TextView to support the delegation of String attribute.

val textView = findViewById<textView>(R.id.textView)

var message: String? by textView

textView.text = "Hello"
println(message)

message = "World"
println(textView.text)

//result:
Hello
World

We delegate message to TextView through delegation. This means that the getter and setter of message will be associated with TextView.

5, Summary

It mainly explains the usage and essence of Kotlin delegation. There are two types of delegation classes and delegation attributes, especially attribute delegation, which should be paid attention to. In fact, there are many scenarios in development that can be simplified by delegation and reduce a lot of duplicate template code. It can be said that it is not inferior to extension.

reference resources

Introduction to kotlin's official website

Kotlin Jetpack actual combat | 07 Entrusted by kotlin

The essence of Kotlin entrustment and the application of MMKV

Kotlin | entrustment mechanism & Principle & Application

Topics: Java Android kotlin