The Road to Kotlintown: Coroutine 66 to Delegate 95

Delegation

"Passing responsibility from one logical grouping of code to another"

"When an object expresses a certain behavior externally but actually delegates implementation of that behavior to a helper object."

"Code reuse through object composition rather than inheritance"

Delegated Properties

  • Forgo the generated getter/setter
  • Instead defer to another object (the delegate) to implement get/set
  • Users still get property syntax

Simple (Silly) Example

Alright, this is a silly example but hopefully a simple one for our first property delegate.

The delegate is implemented as a class, SillyNameDelegate. This delegate will take care of getting and setting the property value through its getValue and setValue property.

import java.util.*
import kotlin.reflect.KProperty
//sampleStart
class SillyNameDelegate {
  private var actualName: String? = null
  
  private val random = Random()
  private val placeholderNames: List<String> =
      listOf("Englebert", "Cheeto", "Mojo", "Protagonist", "Kopfgeschlagen")

  // Getter
  operator fun getValue(thisRef: Any, property: KProperty<*>): String? {
    return actualName ?: placeholderNames[random.nextInt(placeholderNames.size)]
  }

  // Setter
  operator fun setValue(thisRef: Any, property: KProperty<*>, value: String?) {
    actualName = value
  }
}

class SillyUser {
  var name by SillyNameDelegate() // Actual property delegation.
}

fun main(args: Array<String>) {
  val user = SillyUser()
  println(user.name) // random
  println(user.name) // random
  user.name = "Jerry"
  println(user.name) // Jerry
  println(user.name) // Jerry
}
//sampleEnd
  

The actual value is stored inside the delegate as var actualName, a nullable String.

When a property delegates get() to this SillyNameDelegate, the getValue will return either actualName, if it is non-null, or a randomly chosen element of the placeholderNames list.

When a property delegates set() to this SillyNameDelegate, the setValue will take the given value and store it inside actualName.

The actual delegation happens inside the definition of a class. In the SillyUser class, there is a mutable property, name, declared a var. To indicate that the property is delegated, the property type and name are followed with the by keyword and the actual delegate instance. Here, it is a new, inline instance of the SillyNameDelegate.

In the main function, a user is instantiated, and on the first couple accesses of the user's name, random names are returned. After setting the name, that set value is returned on subsequent accesses.


Now all this silly delegates does is to add extra logic to get() to return placeholder values instead of null. Otherwise, it has pretty standard behavior.

So… why wouldn't we just use a custom get()/set() implementation rather than having this whole separate class.

¯\(°_o)/¯

A few reasons. For example, code reuse 👍.


Let's look at some un-DRY code (would that make it WET code?).

Here is a class that represents page data retrieved from a server. We can update parameters that change the page size and data source. When these parameters change, we don't fetch the data right away but set a flag indicating we need a refresh which is executed in the near future.

Of course, this works but even with just two properties, we already have a significant amount of repeated code. Imagine if we kept adding properties. Eek. Boilerplate.

class PagedDataClass {
  var needFetch: Boolean = false
  var pageSize: Int = 6
    set(value) {
      field = value
      needFetch = true
    }
  var dataUrl: String? = null
    set(value) {
      field = value
      needFetch = true
    }
}

Here is a more Android-y example.

In Android apps we may often find ourselves wanting to store user settings in SharedPreferences so that they persist even when the app closes.

To encapsulate the user settings, we might have a UserSettings class with properties for each specific setting that we save to SharedPreferences.

//sampleStart
  class SomeUserSettings(private val sharedPrefs: SharedPreferences) {
  var darkMode: Boolean
    get() = sharedPrefs.getBoolean("darkMode", false)
    set(value) {
      sharedPrefs.edit {
        putBoolean("darkMode", false)
        apply()
      }
    }
    
  var militaryTime: Boolean
    get() = sharedPrefs.getBoolean("militaryTime", false)
    set(value) {
      sharedPrefs.edit {
        putBoolean("militaryTime", false)
        apply()
      }
    }
}
//sampleEnd

In this example as well, everything works correctly but with two properties, we already have a significant amount of code that gets repeated for each property.

How can we make all these boilerplate-filled examples better? With delegated properties!


For the SharedPreferences example, we could create a delegate class to encapsulate a user flag setting that the user can toggle off and on, set to true or false. Then we could use that delegate to more simply implement multiple settings properties.

import java.lang.ref.WeakReference
import kotlin.reflect.KProperty

@Suppress("UNCHECKED_CAST")
class SharedPreferences {
  private val mockPrefs: MutableMap<String, Any?> = mutableMapOf()
  private val listeners: MutableList<WeakReference<OnSharedPreferenceChangeListener>> = mutableListOf()

  fun contains(key: String) = mockPrefs.contains(key)
  fun edit(): Editor = MockEditor(this)
  fun getAll(): Map<String, *> = mockPrefs
  fun getBoolean(key: String, defValue: Boolean): Boolean = mockPrefs[key] as? Boolean ?: defValue
  fun getFloat(key: String, defValue: Float): Float = mockPrefs[key] as? Float ?: defValue
  fun getInt(key: String, defValue: Int): Int = mockPrefs[key] as? Int ?: defValue
  fun getLong(key: String, defValue: Long): Long = mockPrefs[key] as? Long ?: defValue
  fun getString(key: String, defValue: String): String = mockPrefs[key] as? String ?: defValue
  fun getStringSet(key: String, defValue: Set<String>): Set<String> = mockPrefs[key] as? Set<String> ?: defValue
  fun edit(edit: Editor.() -> Unit) { edit.invoke(this.edit()) }
  fun registerOnSharedPreferenceChangeListener(listener: OnSharedPreferenceChangeListener) {
    listeners.add(WeakReference(listener))
  }
  
  fun unregisterOnSharedPreferenceChangeListener(listener: OnSharedPreferenceChangeListener) {
    listeners.find { it.get() == listener }?.let { listeners.remove(it) }
  }

  interface OnSharedPreferenceChangeListener {
    fun onSharedPreferenceChanged(sharedPreferences: SharedPreferences, key: String)
  }

  interface Editor {
    fun apply()
    fun clear(): () -> Boolean
    fun commit(): () -> Boolean
    fun putBoolean(key: String, value: Boolean): Any?
    fun putFloat(key: String, value: Float): Any?
    fun putInt(key: String, value: Int): Any?
    fun putLong(key: String, value: Long): Any?
    fun putString(key: String, value: String): Any?
    fun putStringSet(key: String, values: Set<String>): Any?
    fun remove(key: String): Boolean
  }

  private inner class MockEditor constructor(
      private val sharedPreferences: SharedPreferences
  ) : Editor {
    private val prefsMap = sharedPreferences.mockPrefs
    private val editorMap: MutableMap<String, Any?> = mutableMapOf()
    private val removals: MutableSet<String> = mutableSetOf()

    override fun apply() {
      for (key in removals) {
        prefsMap.remove(key)
      }
      prefsMap.putAll(editorMap)
      val keys = removals.toMutableSet().apply { addAll(editorMap.keys) }
      for (key in keys) {
        for (listener in sharedPreferences.listeners) {
          listener.get()?.onSharedPreferenceChanged(sharedPreferences, key)
        }
      }
    }

    override fun clear() = { removals.addAll(prefsMap.keys) }
    override fun commit() = { apply(); true }
    override fun putBoolean(key: String, value: Boolean) = editorMap.put(key, value)
    override fun putFloat(key: String, value: Float) = editorMap.put(key, value)
    override fun putInt(key: String, value: Int) = editorMap.put(key, value)
    override fun putLong(key: String, value: Long) = editorMap.put(key, value)
    override fun putString(key: String, value: String) = editorMap.put(key, value)
    override fun putStringSet(key: String, values: Set<String>) = editorMap.put(key, values)
    override fun remove(key: String) = removals.add(key)
  }
}


fun main(args: Array<String>) {
  val sharedPrefs: SharedPreferences = SharedPreferences()
  val settings: UserSettings = UserSettings(sharedPrefs)
  resetSettings(settings)
}
//sampleStart

class SharedPrefsFlag(private val sharedPrefs: SharedPreferences) {
  operator fun getValue(thisRef: Any, property: KProperty<*>): Boolean {
    return sharedPrefs.getBoolean(property.name, false)
  }
  operator fun setValue(thisRef: Any, property: KProperty<*>, value: Boolean) {
    sharedPrefs.edit {
      putBoolean(property.name, value)
      apply()
    }
  }
}

class UserSettings(sharedPrefs: SharedPreferences) {
  var darkMode by SharedPrefsFlag(sharedPrefs)
  var militaryTime by SharedPrefsFlag(sharedPrefs)
}

private fun resetSettings(settings: UserSettings) {
  println("darkMode = ${settings.darkMode}")
  println("militaryTime = ${settings.militaryTime}")
  settings.darkMode = true
  settings.militaryTime = true
  println("darkMode = ${settings.darkMode}")
  println("militaryTime = ${settings.militaryTime}")
}
//sampleEnd

 

Exercise for the Reader

Using SharedPrefsBooleanProperty as an example, create a SharedPrefsStringProperty as a delegate for String properties that reads from and writes to SharedPreferences.

You can add the delegate class to the example above and run the example.

Solution

 

import java.lang.ref.WeakReference
import kotlin.reflect.KProperty

@Suppress("UNCHECKED_CAST")
class SharedPreferences {
  private val mockPrefs: MutableMap<String, Any?> = mutableMapOf()
  private val listeners: MutableList<WeakReference<OnSharedPreferenceChangeListener>> = mutableListOf()

  fun contains(key: String) = mockPrefs.contains(key)
  fun edit(): Editor = MockEditor(this)
  fun getAll(): Map<String, *> = mockPrefs
  fun getBoolean(key: String, defValue: Boolean): Boolean = mockPrefs[key] as? Boolean ?: defValue
  fun getFloat(key: String, defValue: Float): Float = mockPrefs[key] as? Float ?: defValue
  fun getInt(key: String, defValue: Int): Int = mockPrefs[key] as? Int ?: defValue
  fun getLong(key: String, defValue: Long): Long = mockPrefs[key] as? Long ?: defValue
  fun getString(key: String, defValue: String): String = mockPrefs[key] as? String ?: defValue
  fun getStringSet(key: String, defValue: Set<String>): Set<String> = mockPrefs[key] as? Set<String> ?: defValue
  fun edit(edit: Editor.() -> Unit) { edit.invoke(this.edit()) }
  fun registerOnSharedPreferenceChangeListener(listener: OnSharedPreferenceChangeListener) {
    listeners.add(WeakReference(listener))
  }
  
  fun unregisterOnSharedPreferenceChangeListener(listener: OnSharedPreferenceChangeListener) {
    listeners.find { it.get() == listener }?.let { listeners.remove(it) }
  }

  interface OnSharedPreferenceChangeListener {
    fun onSharedPreferenceChanged(sharedPreferences: SharedPreferences, key: String)
  }

  interface Editor {
    fun apply()
    fun clear(): () -> Boolean
    fun commit(): () -> Boolean
    fun putBoolean(key: String, value: Boolean): Any?
    fun putFloat(key: String, value: Float): Any?
    fun putInt(key: String, value: Int): Any?
    fun putLong(key: String, value: Long): Any?
    fun putString(key: String, value: String): Any?
    fun putStringSet(key: String, values: Set<String>): Any?
    fun remove(key: String): Boolean
  }

  private inner class MockEditor constructor(
      private val sharedPreferences: SharedPreferences
  ) : Editor {
    private val prefsMap = sharedPreferences.mockPrefs
    private val editorMap: MutableMap<String, Any?> = mutableMapOf()
    private val removals: MutableSet<String> = mutableSetOf()

    override fun apply() {
      for (key in removals) {
        prefsMap.remove(key)
      }
      prefsMap.putAll(editorMap)
      val keys = removals.toMutableSet().apply { addAll(editorMap.keys) }
      for (key in keys) {
        for (listener in sharedPreferences.listeners) {
          listener.get()?.onSharedPreferenceChanged(sharedPreferences, key)
        }
      }
    }

    override fun clear() = { removals.addAll(prefsMap.keys) }
    override fun commit() = { apply(); true }
    override fun putBoolean(key: String, value: Boolean) = editorMap.put(key, value)
    override fun putFloat(key: String, value: Float) = editorMap.put(key, value)
    override fun putInt(key: String, value: Int) = editorMap.put(key, value)
    override fun putLong(key: String, value: Long) = editorMap.put(key, value)
    override fun putString(key: String, value: String) = editorMap.put(key, value)
    override fun putStringSet(key: String, values: Set<String>) = editorMap.put(key, values)
    override fun remove(key: String) = removals.add(key)
  }
}


fun main(args: Array<String>) {
  val sharedPrefs: SharedPreferences = SharedPreferences()
  val settings: UserSettings = UserSettings(sharedPrefs)
  resetSettings(settings)
}
//sampleStart

class SharedPrefsFlag(private val sharedPrefs: SharedPreferences) {
  operator fun getValue(thisRef: Any, property: KProperty<*>): Boolean {
    return sharedPrefs.getBoolean(property.name, false)
  }
  operator fun setValue(thisRef: Any, property: KProperty<*>, value: Boolean) {
    sharedPrefs.edit {
      putBoolean(property.name, value)
      apply()
    }
  }
}

class UserSettings(sharedPrefs: SharedPreferences) {
  var darkMode by SharedPrefsFlag(sharedPrefs)
  var militaryTime by SharedPrefsFlag(sharedPrefs)
}

private fun resetSettings(settings: UserSettings) {
  println("darkMode = ${settings.darkMode}")
  println("militaryTime = ${settings.militaryTime}")
  settings.darkMode = true
  settings.militaryTime = true
  println("darkMode = ${settings.darkMode}")
  println("militaryTime = ${settings.militaryTime}")
}
//sampleEnd