“This is the second day of my participation in the First Challenge 2022, for more details: First Challenge 2022”.

The delegate pattern is a good alternative to inheritance and is a feature of the Kotlin language that can be implemented elegantly. It is often used in development, but often underrated. So let’s make it important today to fully grasp the kotlin delegate nature and principles.

First, delegate class

Let’s start with a delegate class, which is often used to implement the delegate pattern of a class. The key is the by keyword:

interface Base{
  fun print(a)
}

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

class Derived(b: Base): Base by b

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

// Finally output 10
Copy the code

In this delegate pattern, Derived is a wrapper that implements base but doesn’t care how it implements it, delegating the implementation of the interface to its argument db via the keyword BY.

Equivalent to Java code structure:

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

2. Delegate attribute

So the Kotlin delegate class delegates are interface methods, and the delegate property delegates are getters and setters for properties. Kotlin supports the delegate attribute syntax:

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

Property’s get() and set() are delegated to its getValue and setValue methods. Of course, the attribute delegate is not written casually. It must provide a getValue function for the val attribute, and a setValue attribute for the var attribute. Let’s take a look at the official provided Delegate property:

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

We can see that for var modified properties, there must be getValue and setValue methods, and both methods must be modified with the operator keyword.

This first parameter, thisRef, is of type the type of the property owner, or its parent. When we are not sure which class the attribute will belong to, we can define thisRef as Any? .

Moving on to another parameter, property, whose type must be KProperty<*> or its supertype, and whose value is the name of the previous field prop.

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

Let’s test if this is true:

fun main(a) {
    println(Example().prop)
    Example().prop = "Hello, World"
}
Copy the code

You should see the output:

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

2.1 Custom Delegate

Once you know how to write delegate properties, you can implement your own delegate properties as required. However, it is also very troublesome to write so much template code every time we write, so the official also provides an interface class for us to quickly implement:

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

Now the delegated class only needs to implement one of these interfaces. ReadOnlyProperty is used for the val variable, while the var variable implements ReadWriteProperty. Let’s now implement a custom delegate using the ReadWriteProperty interface:

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

Third, the entrustment advanced

3.1 Lazy loading of delegates

Lazy delegate, that is, when we operate on some resource, we want it to be triggered when it is accessed to avoid unnecessary consumption. A lazy() method has been provided to quickly create lazy delegates:

val lazyData: String by lazy {
    request()
}

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

fun main(a) {
    println("Start")
    println(lazyData)
    println(lazyData)
}

/ / the result:Network request Network data Network dataCopy the code

As you can see, only the first call executes the logic in the lambda expression, and subsequent calls only return the final result of the lambda expression.

So how does lazy loading delegate work? Now take a 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)
    }
Copy the code

In this case, the lazy() method takes an argument of type LazyThreadSafetyMod, which defaults to SynchronizedLazyImpl if not passed. It is explained that it is used for multi-thread synchronization, while the other two are not multi-thread safe.

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

  • Lazythreadsafetymode.none: This can be used if the initialization will always occur in the same thread as the property use, but it has no synchronization lock.

Let’s focus now on what’s going on in SynchronizedLazyImpl:

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 vallock = lock ? :this

    override val value: T
        get() {
            val _v1 = _value
            // Check whether it has been initialized. If it has been initialized, it returns directly, and does not call the internal logic of the advanced function
            // If the two values are different, the current value has already been loaded
            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 a high-level function to get its return value
                    valtypedValue = initializer!! (a)// Assign the return value to _value, which is used to return the return value of the advanced function directly at the next judgment
                    _value = typedValue
                    initializer = null
                    typedValue
                }
            }
        }
			......
}
Copy the code

The code above shows that SynchronizedLazyImpl overrides the return value of the lazy interface and overrides the accessor for the property, using logic similar to Java’s double check. But how does a Lazy interface become a delegate attribute?

The lazy.kt file declares the Lazy interface’s getValue extension, which will be called when the final assignment is made, and we said in the custom delegate that we need to provide a getValue function for the val attribute.

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

With this lazy delegate, it’s much easier to implement the singleton:

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

Delegates to the observer

If you want to watch a property change, Delegates the property to Delegates. Observable, which has three parameters: assigned property, old value and new value:

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

Returns an ObservableProperty object, inherited from ReadWriteProperty. Take a 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)
        }
Copy the code

InitialValue is the initialValue, and the other argument onChange is the callback handler when the property value is modified.

3.3 By Map Mapping delegation

A common use case is to store the value of an attribute in a map, which can use map /MutableMap to delegate attributes:

class User(valmap: 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 LoLCopy the code

Key $Key is missing in the Map: $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.")})}Copy the code

So when you use it, you have to have a mapping value.

3.4 Direct delegation between two properties

Starting with Kotlin 1.4, we can directly delegate “attribute A” to “attribute B” syntactically, as shown in the following example:

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

The value of total is exactly the same as count because we delegate the getter and setter for total to count. The logic can be explained in code:

class Item {
    var count: Int = 0

    var total: Int
        get() = count

        set(value: Int) {
            count = value
        }
}
Copy the code

Delegate names can be written using the “:” qualifier, such as this:: Delegate or MyClass:: Delegate.

This usage is useful when a field is changed and the original field is preserved. You can define a new field and then delegate it to the original field so you don’t have to worry about different values.

3.5 Providing Delegation

What if I need to do some extra judgment work before binding the property delegate? We can define a provideDelegate to implement this:

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> {
	// Different initial values are passed in according to the name of the attribute 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(a) {
    val owner = Owner()
    println(owner.normalText)
    println(owner.logText)
}

/ / the result:
normal
log
Copy the code

Here we create a new SmartDelegator, apply another layer to the member method provideDelegate, and then make some logical decisions in it before delegating the property to getStringDelegate.

This ability to intercept the binding between an attribute and its delegate greatly shorts the logic that requires passing the attribute name to achieve the same functionality.

Four, entrust chestnuts

4.1 Simplifying Fragment/Activity Parameter Transfer

Fragments on mass participation at ordinary times, each time to write a large section of the code is very vexed, and now have to entrust this magic weapon is to simplify it together, 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)}}}Copy the code

If the value is null, the delegate class will be used to delegate the value. If the value is null, the delegate class will be null.

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

It looks like a lot less code, isn’t it amazing? Here’s how to do it:

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

    override fun getValue(thisRef: Fragment, property: KProperty< * >): T {
      // Bunndle values are handled separately
        returnthisRef.arguments? .getValue(property.name)as? T ? :throw IllegalStateException("Property ${property.name} could not be read")}override fun setValue(thisRef: Fragment, property: KProperty<*>, value: T) {
        valarguments = thisRef.arguments ? : Bundle().also { thisRef.arguments = it }if (arguments.containsKey(property.name)) {
            // The Value is not expected to be modified
            return
        }
      	// Bunndle Settings are handled separately
        arguments[property.name] = value
    }
}

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

4.2 Simplifying SharedPreferences

Wouldn’t it be convenient if we saved the value now:

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

// Display cache
display(spResponse)

// Update the cache
spResponse = response
Copy the code

The answer is yes, or use the delegate attribute to change, the following is a concrete 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 {
        returngetPref(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()
        }
    }
}
Copy the code

4.3 Binding of data and View

Once you have a delegate, you can bind data to a View without having to go to 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
    }
}
Copy the code

Write an extension function to our TextView that supports the String delegate.

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

var message: String? by textView

textView.text = "Hello"
println(message)

message = "World"
println(textView.text)

/ / the result:
Hello
World
Copy the code

We’re delegating a Message to a textView by way of a delegate. This means that both getters and setters for the Message will be associated with the TextView.

Five, the summary

Mainly explains the usage and nature of Kotlin delegate, there are two types of delegate class and delegate attribute, especially attribute delegate should be valued. There are plenty of scenarios in development that can be simplified by using delegates, eliminating a lot of repetitive boilerplate code, and arguably rivaling extensions.

reference

Kotlin official website commissioned introduction

Kotlin Jetpack combat | 07. Kotlin entrust

The nature of Kotlin delegate and the application of MMKV

Kotlin | entrusted mechanism & principle & application