Preface
The delegate mode is a good alternative to implementing inheritance, and it is also a feature of the Kotlin language, which can elegantly implement the delegate mode. It is also indispensable during development, but it is often underestimated. So today let it be taken seriously and fully grasp the characteristics and principles of kotlin delegation.
1. Entrustment category
We first complete a delegate class, which is often used to implement the class's delegate pattern. The 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 output10
In this delegate mode, Derived is equivalent to a wrapper. Although it also implements base, it does not care how it is implemented. Through the keyword by, the interface implementation is delegated to its parameter db.
The equivalent structure of Java code:
class Derived implements Base{ Base b; public Derived(Base b){ = b} }
2. Delegation attributes
As mentioned earlier, Kotlin delegate class is a delegate interface method, and delegate attribute delegate is a property getter and setter method. Delegate attribute syntax supported by kotlin:
class Example { var prop: String by Delegate() }
The corresponding get() and set() of the attribute will be delegated to its getValue and setValue methods. Of course, the delegate 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 provide a setValue attribute. Let’s first look at the official delegate attribute Delegate:
class Delegate { operator fun getValue(thisRef: Any?, property: KProperty<*>): String { return "$thisRef, thank you for delegating '${}' to me!" } operator fun setValue(thisRef: Any?, property: KProperty<*>, value: String) { println("$value has been assigned to '${}' in $thisRef.") } }
We can see that for the properties of var modification, there must be 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 owner of this property, 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? Now.
Next, 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, its type must be the type of the delegate attribute, or its parent class. That is to say, the value: String in the example can also be replaced with value: Any.
Let's test if this is true:
fun main() { println(Example().prop) Example().prop = "Hello, World" }
Then 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 Custom Delegation
After knowing how to write the delegate attribute, you can also implement your own attribute delegation according to your needs. However, it is also very troublesome to write so much template code every time you write, so the official also provides interface classes 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) }
Now the delegated class only needs to implement one of the interfaces. Use ReadOnlyProperty for val variables, while the var variable implements ReadWriteProperty. We will now 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. Entrustment advancement
3.1 Lazy loading delegates
Lazy loading of the delegation means that when we operate some resources, we hope that 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 loading delegation:
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 Perform network requests Network data Network data Copy the code
You can see that only the first call will execute the logic in the lambda expression, and the subsequent call will only return the final result of the lambda expression.
So how do you implement lazy loading delegates? Now let’s 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) { -> SynchronizedLazyImpl(initializer) -> SafePublicationLazyImpl(initializer) -> UnsafeLazyImpl(initializer) }
In this case, the lazy() method will receive a LazyThreadSafetyMod parameter. If this parameter is not passed, the SynchronizedLazyImpl method will be used by default. By looking at the explanation, you can know that it is used for multi-thread synchronization, while the other two are not multi-thread safe.
: The initialization method can be called multiple times, but the value is only the return value when the first return is returned, that is, only the first return value can be assigned to the initialized value.
LazyThreadSafetyMode. NONE: If initialization will always happen in the same thread as the property is used, it can be used in this case, but it does not have a synchronous lock.
Let's take a look at what is done 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 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 return directly, and do not call the internal logic of the advanced function. //If these two values are not the same, it means that the current value has been loaded and returns 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 { //Calling advanced function to get its return value val typedValue = initializer!!() // Assign the return value to _value, and it is used to directly return the return value of the advanced function during the next judgment. _value = typedValue initializer = null typedValue } } } ...... }
Through the above code, we can find that SynchronizedLazyImpl overrides the return value of the lazy interface and rewritten the accessor of the attribute. The specific logic is similar to Java's double verification. But how does the Lazy interface become a delegate attribute?
I found in the file that declares the getValue extension property of the Lazy interface, which will be called when the final assignment is final. As we said in the custom delegate, for the val property, we need to provide a getValue function.
## //This extension allows attribute delegates using instances of Lazypublic inline operator fun <T> Lazy<T>.getValue(thisRef: Any?, property: KProperty<*>): T = value
With this lazy loading delegation, it becomes easier for us to implement singletons:
class SingletonDemo private constructor() { companion object { val instance: SingletonDemo by lazy{ SingletonDemo() } } }
3.2 Observer delegation
If you want to observe the change of a property, you can delegate the property to it, which has three parameters: the assigned property, the old value and the new value:
var name: String by ("<no name>") { prop, old, new -> println("$old -> $new") }
Returns an ObservableProperty object inherited from ReadWriteProperty. Let's 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) }
initialValue is the initial value, and another parameter onChange is the callback processor when the property value is modified.
3.3 by map map delegate
A common use case is to store the value of an attribute in a map, which can use Map/MutableMap to implement attribute delegates:
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() map["name"] = "LOL" println() } //Output:Ha ha LoL
However, there will be a problem during use. If there is no mapping value of the delegate attribute name in the map, an exception will be thrown when the value is taken: Key $key is missing in the map:
## public inline operator fun <V, V1 : V> MutableMap<in String, out @Exact V>.getValue(thisRef: Any?, property: KProperty<*>): V1 = (getOrImplicitDefault() as V1) @ public inline operator fun <V> MutableMap<in String, in V>.setValue(thisRef: Any?, property: KProperty<*>, value: V) { (, value) } ## internal fun <K, V> Map<K, V>.getOrImplicitDefault(key: K): V { if (this is MapWithDefault) return (key) return getOrElseNullable(key, { throw NoSuchElementException("Key $key is missing in the map.") }) }
Therefore, when using it, you must pay attention to having mapped values.
3.4 Direct delegate between two attributes
Starting from Kotlin 1.4, we can delegate "Property A" to "Property B" directly 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 is exactly the same as count, because we delegate the getter and setter of the total attribute to count. The specific logic can be explained in code:
class Item { var count: Int = 0 var total: Int get() = count set(value: Int) { count = value } }
In writing, the delegate name can be written using the ":" qualifier, such as this::delegate or MyClass::delegate.
This usage is very useful when the field changes 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 new and old fields.
3.5 Provide a delegation
What should I do if I want to do some extra judgment before binding the attribute delegate? 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> { //Pause different initial values according to the name of the attribute delegate return if (("log")) { StringDelegate("log") } else { StringDelegate("normal") } } } class Owner { var normalText: String by SmartDelegator() var logText: String by SmartDelegator() } fun main() { val owner = Owner() println() println() } //result:normal log
Here we create a new SmartDelegator, and then set a layer of the member method provideDelegate, then make some logical judgments in it, and finally delegate the property to getStringDelegate.
This ability to intercept the binding between attributes and their delegates greatly shortens the logic that the attribute name must be passed to achieve the same function.
4. Entrust Ritsuko
4.1 Simplify Fragment/Activity Transfer Parameters
I usually pass parameters on Fragment, and Isn't it annoying to write a large piece of code every time? Now that I have the magic weapon of delegation, I will simplify it together. The normal mode is as follows:
class BookDetailFragment : Fragment(.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?) { (savedInstanceState) arguments?.let { bookId = (EXTRA_book_ID, 123) bookType = (EXTRA_BOOK_TYPE, 1) } } }
After writing a long paragraph, I finally wrote the basic method of passing parameters. When getting the value, I also have to deal with the situation where the parameters are empty. Now we will extract the delegate class and use the attribute delegate to re-implement the above function:
class BookDetailFragment : Fragment(.fragment_book_detail) { private var bookId: Int by argument() companion object { fun newInstance(bookId: Int, bookType: Int) = BookDetailFragment().apply { = bookId } } override fun onViewCreated(root: View, savedInstanceState: Bundle?) { ("tag", "BOOKID:" + bookId); } }
It seems that a lot of code has been reduced, isn't it very magical? The following implementation ideas are as follows:
class FragmentArgumentProperty<T> : ReadWriteProperty<Fragment, T> { override fun getValue(thisRef: Fragment, property: KProperty<*>): T { //The Bunndle value must be processed separately return ?.getValue() as? T ?: throw IllegalStateException("Property ${} could not be read") } override fun setValue(thisRef: Fragment, property: KProperty<*>, value: T) { val arguments = ?: Bundle().also { = it } if (()) { // The Value is not expected to be modified return } //The Bunndle setting must be processed separately arguments[] = value } } fun <T> (defaultValue: T? = null) = FragmentArgumentProperty(defaultValue)
4.2 Simplify SharedPreferences access values
Isn't it very convenient if we can now access the value:
private var spResponse: String by PreferenceString(SP_KEY_RESPONSE, "") // Read, display cachedisplay(spResponse) // Update cachespResponse = response
Is the answer OK or use the delegate attribute to transform it? 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 = ) { 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(()) { 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
After the delegation is available, data can also be bound to the View without DataBinding.
operator fun (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 delegate of the String property.
val textView = findViewById<textView>() var message: String? by textView = "Hello" println(message) message = "World" println() //result:Hello World
We delegated message to textView by delegating. This means that both 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 categories and delegation attributes, especially attribute delegations that should be worthy of attention. In development, there are actually many scenarios that can be simplified with delegates and reduce a lot of duplicate boilerplate code, which can be said to be no less than extensions.
refer to
Kotlin official website entrustment introduction
Kotlin Jetpack Practical | 07. Kotlin Commission
The nature of Kotlin delegation and the application of MMKV
Kotlin | Delegation Mechanism & Principles & Applications
Summarize
This is all about this article about Kotlin commission. For more information about Kotlin commission, please search for my previous articles or continue browsing the related articles below. I hope everyone will support me in the future!