The delegate pattern has proved to be a good alternative to inheritance, and Kotlin provides elegant support for the delegate pattern at the language level (syntactic sugar).

Let’s take a look at a useful piece of work I’ve done in the Android project using Kotlin’s attribute delegate syntax — read and write delegate for SharedPreferences.

All the code displayed in this article has been uploaded to Github as a Demo. Click here to get the source code. You are advised to read this article with Demo.

The main file structure of the project is as follows:

│ ├─ lap-├ ─ lap-├ ─ lap-├ ─ lap-├ ─ lap-├ ─ lap-├ ─ lap-├ ─ lap-├ ─ lap-├. Lap-├ ─ lap-├ ─ lap-├. Lap-├ ─ lap-├. Lap-├ ─ lap-├. Lap-├ ─ lap-├. Lap-├ ─ lap-├. Lap-├ ─ lap-├Copy the code

Well, if there’s a file in the lapel.

SPUtils is a basic utility class for reading and writing SharedPreferences stored items:

/ * * *@author xiaofei_dev
 * @descBasic tool class */ for reading and writing SP storage items

object SPUtils {
    val SP by lazy {
        App.instance.getSharedPreferences("default", Context.MODE_PRIVATE)
    }

    // Read the SP storage entry
    fun <T> getValue(name: String.default: T): T = with(SP) {
        val res: 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 java.lang.IllegalArgumentException()
        }
        @Suppress("UNCHECKED_CAST")
        res as T
    }

    // Write the SP storage entry
    fun <T> putValue(name: String, value: T) = with(SP.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 can't be saved into Preferences")
        }.apply()
    }
}
Copy the code

The main use of generics to achieve a complete SP read and write, the overall is very concise and easy to understand. The context object uses a custom instance of the Application class (see App class in Demo).

Delegate properties in Kotlin

The definition of SPDelegates:

/ * * *@author xiaofei_dev
 * @desc<p> The lightweight delegate class that reads and writes SP items is delegated to the getValue method and writes SP to the setValue method. Note that these two methods are not explicitly called. Leave everything to the compiler (again, syntax sugar) * See the SpBase file </p> */ for the code to define SP storage items using this class

class SPDelegates<T>(private val key: String, private val default: T) : ReadWriteProperty<Any? , T> {override fun getValue(thisRef: Any? , property:KProperty< * >): T {
        return SPUtils.getValue(key, default)}override fun setValue(thisRef: Any? , property:KProperty<*>, value: T) {
        SPUtils.putValue(key, value)
    }
}
Copy the code

Delegates the SPDelegates class implements the ReadWriteProperty interface declared in the Kotlin standard library for property delegates, known by its name as readable and writable (for var declared properties), In addition, there is a ReadOnlyProperty (read-only) interface for properties declared by val.

For the delegate class of attributes (SPDelegates for example), a getValue() function (and a setValue() function — for the var attribute — is required to be provided. Parameter requirements of getValue method are as follows:

  • thisRef– must be connected toAttribute classIs the same type (for extended attributes — the type being extended) or its supertype (see comments in the SpBase singleton class below);
  • property— Must be a typeKProperty<*>(a class in the Kotlin library kotlin.Reflect package) or its supertype.

For its setValue method, the first two arguments are the same as getValue. The third argument, value, must be of the same type or subtype as the property.

It doesn’t matter if you don’t understand the above concepts for the time being, let’s use the delegate attribute to deepen your understanding.

Next comes the SpBase singleton class that uses the delegate property specifically:

/ * * *@author xiaofei_dev
 * @descSP storage item */ defined
object SpBase{
    //SP stores the key of the item
    private const val CONTENT_SOMETHING = "CONTENT_SOMETHING"


    // This defines an SP store item
    // Delegates the writing and writing of SP to an instance of the SPDelegates class using the by word, a Kotlin language primitive,
    // To access SpBase's contentSomething (which you can simply think of as a static variable in Java) property,
    // Assigning to the contentSomething property is writing SP, that's all
    The type of thisRef (see above) parameter of the getValue method of the SPDelegates object is the outer SpBase
    var contentSomething: String by SPDelegates(CONTENT_SOMETHING, "I'm an SP store item, click edit me")}Copy the code

In the code above, the singleton SpBase property contentSomething is a defined SP store entry. Thanks to powerful syntax sugar support at the language level, written code can be concise and elegant. The request to read or write the SP store item is delegates to a SPDelegates object via property the delegate property has the syntax of

Var < attribute name >: < type > by < expression >

This is eventually interpreted by the compiler as code like this (roughly) :

object SpBase{
    private const val CONTENT_SOMETHING = "CONTENT_SOMETHING"
    
    private val propDelegate = SPDelegates(CONTENT_SOMETHING, "I'm an SP store item, click edit me")
    var contentSomething: String
        get() = propDelegate.getValue(this.this::contentSomething)/ / read SP
        set(value) = propDelegate.setValue(this.this::contentSomething, value)/ / write SP
}
Copy the code

It’s easier to understand. Here is a demonstration of how to use the defined SP storage item, as shown in the Demo MainActivity class file:

class MainActivity : AppCompatActivity() {

    override fun onCreate(savedInstanceState: Bundle?). {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)
        initView()
    }

    private fun initView(a){
        // Read the SP content and display it on the interface
        editContent.setText(SpBase.contentSomething)
        btnSave.setOnClickListener {
            // Save the SP entry
            SpBase.contentSomething = "${editContent.text}"
            Toast.makeText(this, R.string.main_save_success, Toast.LENGTH_SHORT).show()
        }
    }
}
Copy the code

Overall relatively simple, is a process of reading and writing SP storage items. You can actually run the Demo and see how it works.

A delegate class that implements an attribute from zero

The SPDelegates class implements the ReadWriteProperty interface provided by the Kotlin library, of course we can implement a property delegate class without any interface, As long as it provides a getValue() function (and a setValue() function — for the var attribute — and meets the parameter requirements we discussed above. Let’s define a simple property Delegate (see the Example file in the Demo package) :

/ * * *@author xiaofei_dev
 * @descDelegate class */ without implementing any of the interface's trivial properties

class Delegate<T> {
    private var value: T? = null

    operator fun getValue(thisRef: Any? , property:KProperty< * >): T? {
        println("$thisRef, thank you for delegating '${property.name}' to me! The value is $value")
        return value
    }

    operator fun setValue(thisRef: Any? , property:KProperty<*>, value: T?). {
        this.value = value
        println("$value has been assigned to '${property.name}' in $thisRef.")}}Copy the code

Use the same way:

class Example {
    // Delegate properties
    var p: String? by Delegate()
}

fun main(args: Array<String>) {
    val e = Example()
    e.p = "hehe"
    println(e.p)
}
Copy the code

The console output is as follows:

hehe has been assigned to 'p' in com.xiaofeidev.delegatedemo.demo.Example@1fb3ebeb.
com.xiaofeidev.delegatedemo.demo.Example@1fb3ebeb, thank you for delegating 'p' to me! The value is hehe
hehe
Copy the code

You can run down to try ~

About the Delegate model

A separate section is necessary to explain what delegation is.

In a nutshell, in the delegate pattern, two objects work on the same request, and the receiving object delegates the request to the other object.

The simplest example of the delegate pattern is:

// delegate type, ink can be used to print text (~ ▽ ~)"
class Ink {
    fun print(a) {
        print("This message comes from the delegate class,Not Printer.")}}class Printer {
    // Delegate object
    var ink = Ink()

    fun print(a) {
        //Printer's instance delegates the request to another object (DelegateNormal's object) to handle it
        ink.print()// Call the delegate object's method}}fun main(args: Array<String>) {
    val printer = Printer()
    printer.print()
}
Copy the code

The console output is as follows:

This message comes from the delegate class,Not Printer.
Copy the code

The delegation pattern allows us to substitute aggregation for inheritance and is the basis for many other design patterns, such as the state pattern, the policy pattern, and the visitor pattern.

Kotlin’s delegation model

Kotlin can implement the delegate pattern with zero boilerplate code (rather than requiring boilerplate code as shown above)!

For example, we now have the following interfaces and classes:

interface Base {
    fun print(a)
}

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

What the Base interface wants to do is print something on the console. That’s fine, we’ve fully implemented the Base interface on the BaseImpl class.

If we want to write another implementation of the Base interface, we can do this:

class Derived(b: Base) : Base by b
Copy the code

This is actually equivalent to what the compiler actually generates:

class Derived(val delegate: Base) : Base {
    override fun print(a) {
        delegate.print()
    }
}
Copy the code

Note not the following:

class Derived(val delegate: Base){
    fun print(a) {
        delegate.print()
    }
}
Copy the code

Kotlin used the compiler’s dark magic to seal a lot of boilerplate code into a language-level primitive like BY (again, syntactic sugar). Usage:

fun main(args: Array<String>) {
    val b = BaseImpl(10)
    Derived(b).print()
}
Copy the code

The console output is as follows:

10
Copy the code

Other property delegates in the Kotlin library

Going back to attribute delegates, Kotlin’s standard library has factory methods for some common delegates, listed below.

The delay attribute Lazy

fun main(args: Array<String>) {
    The logic in a lambda expression after lazy executes only once (and is thread-safe) and records the result, and the subsequent get() method that calls the property simply returns the result of the record
    val lazyValue: String by lazy {
        println("computed!")
        "Hello"
    }
    println(lazyValue)
    println(lazyValue)
}
Copy the code

The console output is as follows:

computed!
Hello
Hello
Copy the code

Observable property Observable

Delegates. Observable () accepts two parameters: initial value and modifity-time handler. This handler is called each time an attribute is assigned (executed after the assignment). The handler takes three parameters: the KProperty object to which the attribute is assigned, the old value, and the new value:

class User {
    var name: String by Delegates.observable("<no name>") {
            prop, old, new ->
        println("$old -> $new")}}fun main(args: Array<String>) {
    val user = User()
    user.name = "first"
    user.name = "second"
}
Copy the code

The console output is as follows:

<no name> -> first
first -> second
Copy the code

Store attributes in a map

You can even store property values in a map. In this case, you can delegate attributes directly to the mapping instance:

class Student(valmap: Map<String, Any? >) {val name: String by map
    val age: Int     by map
}

fun main(args: Array<String>) {
    val student = Student(mapOf(
        "name" to "xiaofei"."age"  to 25
    ))

    println(student.name)
    println(student.age)
}
Copy the code

Of course the application must make sure that the name of the attribute matches the key in the map, otherwise you might get a NoSuchElementException runtime exception that looks something like this:

java.util.NoSuchElementException: Key XXXX is missing in the map.
Copy the code

Here, to be continued.

The resources

  1. Design Patterns: The Foundation of Reusable Object-oriented Software
  2. Kotlin for Android Developers
  3. Kotlin Minimalist Tutorial
  4. Kotlin Core Programming
  5. Wikipedia delegate schema entries
  6. Kotlin official documentation