The articles

Kotlin Jetpack: The Beginning

00. The Kotlin Pit Guide for Java Developers

03. Kotlin programming’s Triple Realm

04. Kotlin higher-order Functions

05. Kotlin Generics

06. Kotlin Extension

1. Introduction

Delegation is probably the most underrated feature of Kotlin.

When it comes to Kotlin, the first thing that comes to mind is extension, followed by coroutines, and then empty security. However, in certain scenarios, delegation can be extremely useful.

This article will systematically introduce Kotlin’s delegation, and then in the actual practice, I will try to use delegation + extension functions + generics to encapsulate a relatively complete SharedPreferences framework.

2. Prepare

  • Update the Android Studio version to the latest
  • Clone our Demo project locally and open it with Android Studio:

Github.com/chaxiu/Kotl…

  • Switch to branch:chapter_07_delegate
  • I strongly suggest that you follow this article to practice, the actual combat is the essence of this article

3. Class Delegation

Delegate classes, with the keyword BY, make it easy to implement syntax-level delegate patterns. Here’s a simple example:

interface DB {
    fun save(a)
}

class SqlDB() : DB {
    override fun save(a) { println("save to sql")}}class GreenDaoDB() : DB {
    override fun save(a) { println("save to GreenDao")}}// The argument delegates the interface implementation to DB via BY
/ / left left
class UniversalDB(db: DB) : DB by db

fun main(a) {
    UniversalDB(SqlDB()).save()
    UniversalDB(GreenDaoDB()).save()
}

SQL save to GreenDao */
Copy the code

This delegation pattern is so common in our actual programming that UniversalDB is a shell that provides database storage functionality but doesn’t care how it is implemented. Whether you want to use Sql or GreenDao, just pass different delegate objects into it.

The above delegate class is equivalent to the following Java code:

class UniversalDB implements DB {
    DB db;
    public UniversalDB(DB db) { this.db = db; }
             // Manually override the interface to delegate save to db.save()
    @Override/ / left
    public void save(a) { db.save(); }}Copy the code

In the example above, the interface has only one method, so Java doesn’t seem too cumbersome. However, when we want to delegate many interface methods, this BY can greatly reduce the amount of code we need.

Let’s look at a slightly more complicated example. Suppose we want to encapsulate a MutableList and add a method, using the delegate class by, in a few lines:

// This parameter does the work. All interface implementations are delegated to it
/ / left
class LogList(val log: () -> Unit.val list: MutableList<String>) : MutableList<String> by list{
    fun getAndLog(index: Int): String {
        log()
        return get(index)
    }
}
Copy the code

In Java, we have to implements so many methods:

It’s tiring to think about writing all that code again and again, isn’t it?

Note: Effective Java mentions:Composition over InheritanceFavor Composition over Inheritance, so in Java, we use interfaces as much as possible. With the delegate classes provided by Kotlin, it’s easier to use composite classes. Combined with the above example, delegate classes can really save a lot of code if there are multiple interfaces to implement.

4. Property Delegation

Delegate properties, which and delegate classes are both used by by, but they are completely different. A delegate class delegates its interface implementation; Delegate properties, delegate out getters, setters for properties. Val text = by lazy{} delegate the getter for text to lazy{}.

Val text: String get() {lazy{}} val text: String get() {lazy{}}Copy the code

5. Customize delegate properties

Kotlin’s delegate property works wonders, so how do we implement our own delegate property on demand? Consider the following example:

class Owner {
    varText: String = "Hello"}Copy the code

I want to delegate to the text property above. What should I do? Take a look at the comments for the example below:

class StringDelegate(private var s: String = "Hello") {
// The class corresponding to text corresponds to the type of text
/ / ⚑ πŸ‘‡ left
    operator fun getValue(thisRef: Owner, property: KProperty< * >): String {
        return s
    }
// The class corresponding to text corresponds to the type of text
/ / ⚑ πŸ‘‡ left
    operator fun setValue(thisRef: Owner, property: KProperty<*>, value: String) {
            s = value
    }
}

/ / πŸ‘‡
class Owner {leftvar text: String by StringDelegate()
}
Copy the code

Summary:

  • varB: We need to providegetValueandsetValue
  • val— onlygetValue
  • operatorThis is the key for the compiler to recognize delegate attributes. The comments are marked with ⚑.
  • propertyIts type is usually written in a fixed wayKProperty<*>
  • valueThe type of — must beEntrusted propertyType, or its superclass. That is, in the examplevalue: StringYou can also change it tovalue: Any. Used in commentsleftThe annotation.
  • thisRef— It must be of typeProperty ownerType, or its superclass. That is, in the examplethisRef: OwnerYou can also change it tothisRef: Any. The comments are marked with πŸ‘‡.

The above isEntrusted propertyThe more important details, master these details, we write custom delegate no problem.

6. The actual combat

It’s time for our familiar practice, so let’s do something interesting.

Warm up 7.

In the previous section, we implemented a simple HTML DSL. Let’s see how you can optimize the code using delegates. If you look closely, you can see that this code looks very uncomfortable. The template code is obvious:

class IMG : BaseElement("img") {
    var src: String
        get() = hashMap["src"]!!!!!set(value) {
            hashMap["src"] = value
        }
/ / write
// Look at the duplicate template code
/ / left
    var alt: String
        get() = hashMap["alt"]!!!!!set(value) {
            hashMap["alt"] = value
        }
}
Copy the code

It would be nice to write it with a delegate property:

// This code looks very comfortable
class IMG : BaseElement("img") {
    var src: String by hashMap
    var alt: String by hashMap
}
Copy the code

Following the custom delegate attributes described earlier, we could easily write code like this:

// corresponds to the IMG class
/ / πŸ‘‡
operator fun HashMap
       ,>.getValue(thisRef: IMG, property: KProperty< * >): String? =
        get(property.name)
    
operator fun HashMap<String, String>.setValue(thisRef: IMG, property: KProperty<*>, value: String) =
        put(property.name, value)
Copy the code

ThisRef can be a parent class, so it is not a problem to write thisRef like this:

// The change is here
/ / πŸ‘‡
operator fun HashMap
       ,>.getValue(thisRef: Any, property: KProperty< * >): String? =
        get(property.name)

operator fun HashMap<String, String>.setValue(thisRef: Any, property: KProperty<*>, value: String) =
        put(property.name, value)
Copy the code

ThisRef: Any, we can delegate Any String attribute in this way, for example:

class Test {
    var src: String by hashMap
    var alt: String by hashMap
}
Copy the code

There is 1

Excuse me: abovethisRef: AnytothisRef: Any?Would it be better? Why is that?

There is 2

There is an official implementation of the map delegate. What’s so good about the official way of writing it? (The answer lies inGitHub DemoIn the code comments.

Delegate attribute + SharedPreferences

In the previous chapter on extension functions, we simplified SharedPreferences by using higher-order functions plus extension functions, but that usage is still not as neat as the way we used it at the time, and frankly, not as good as our Java package PreferenceUtils.

// MainActivity.kt
private val preference: SharedPreferences by lazy(LazyThreadSafetyMode.NONE) {
    getSharedPreferences(SP_NAME, MODE_PRIVATE)
}

// Read cache
private val spResponse: String? by lazy(LazyThreadSafetyMode.NONE) {
    preference.getString(SP_KEY_RESPONSE, "")}private fun display(response: String?). {
    // Update the cache
    preference.edit { putString(SP_KEY_RESPONSE, response) }
}
Copy the code

What if we could do this:

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

// Display cache
display(spResponse)

// Update the cache
spResponse = response
Copy the code

This is wonderful!

A delegate property like this is actually pretty easy to implement, right?

operator fun getValue(thisRef: Any? , property: KProperty<*>): String { return prefs.getString(name, "") ? : default } operator fun setValue(thisRef: Any? , property: KProperty<*>, value: String) { prefs.edit().apply() }Copy the code

To make it support the default, commit(), we add two arguments:

class PreferenceString(
        private val name: String,
        private val default:String ="".private val isCommit: Boolean = false.private val prefs: SharedPreferences = App.prefs) {

    operator fun getValue(thisRef: Any? , property:KProperty< * >): String {
        returnprefs.getString(name, default) ? : default }operator fun setValue(thisRef: Any? , property:KProperty<*>, value: String) {
        with(prefs.edit()){
            putString(name, value)
            if (isCommit) {
                commit()
            } else {
                apply()
            }
        }
    }
}
Copy the code

That’s easy, right?

The above code only supports String. To make our framework support different types of parameters, we can introduce generics:

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

All of the above code is on GitHub. Welcome to Star Fork: github.com/chaxiu/Kotl… .

For details of the Delegation test code see thisGitHub Commit

Delegation HTML for details see thisGitHub Commit

Delegation SharedPreferences for details see thisGitHub Commit

Question 3:

The code above supports only a few basic types. Can you extend it to support more types?

There is 4:

With this encapsulation, next time we want to add other framework support for PreDelegate, such as Tencent’sMMKVWhat should be done?

There is 5:

Is there anyMore elegantHow to encapsulate SharedPreferences?

9. The ending:

  • Commission, divided intoDelegate class.Entrusted property
  • Delegate classes can be implemented quickly and easilyDelegate pattern, can also be implemented with the interfaceKind of combination
  • Delegate attributes, which can improve codeThe reuse rate ofCan also improve the codereadability.
  • Delegate class, which is basically what the compiler will doconsignor.delegateeThe corresponding interface methodsThe binding.
  • Delegate property, which works because the compiler recognizes specificgetter.setterIf they meet specific signature requirements, they are resolved toDelegation

All see this, give it a thumbs up!

【Kotlin Jetpack 】