Delegation and delegated properties

Kotlin · Advanced · 12 min

Delegation replaces much inheritance and boilerplate, and lazy initialization is used everywhere.

Class delegation implements an interface by forwarding to another object:

class LoggingList<T>(private val inner: MutableList<T>) : MutableList<T> by inner {
    override fun add(element: T): Boolean { println("add $element"); return inner.add(element) }
}

The compiler generates all the forwarding methods; you override only what you want to change. This is the "composition over inheritance" pattern without the boilerplate.

Delegated properties hand a property's get/set logic to a delegate object: val x: T by delegate. Standard delegates:

  • by lazy { ... }: computed on first access, then cached. Thread-safe by default (LazyThreadSafetyMode.SYNCHRONIZED).
  • by Delegates.observable(initial) { prop, old, new -> }: runs a callback after each change.
  • by Delegates.vetoable(initial) { _, old, new -> condition }: rejects changes that fail the check.
  • by map: reads properties from a Map<String, Any?> by name, handy for dynamic data.
  • by ::otherProperty: delegates to another property (for renames/deprecations).

You can write your own delegate: any object with operator fun getValue(thisRef, property) (and setValue for var), or implement ReadOnlyProperty / ReadWriteProperty. The property.name lets one delegate class serve many properties, as in preference or settings wrappers.

Android's by viewModels() and Compose's by remember { mutableStateOf() } are delegated properties too.

Example

import kotlin.properties.Delegates
import kotlin.properties.ReadWriteProperty
import kotlin.reflect.KProperty
class Trimmed : ReadWriteProperty<Any?, String> {
    private var value = ""
    override fun getValue(thisRef: Any?, property: KProperty<*>) = value
    override fun setValue(thisRef: Any?, property: KProperty<*>, value: String) { this.value = value.trim() }
}
class Profile(data: Map<String, Any?>) {
    val name: String by data                  // read from the map by property name
    val slug: String by lazy { println("computing"); name.lowercase() }
    var status: String by Delegates.observable("new") { _, old, new -> println("$old -> $new") }
    var bio: String by Trimmed()
}
class CountingSet<T>(private val inner: MutableSet<T> = HashSet()) : MutableSet<T> by inner {
    var attempts = 0; override fun add(element: T): Boolean { attempts++; return inner.add(element) }
}
fun main() {
    val p = Profile(mapOf("name" to "Ada"))
    println(p.slug); println(p.slug)          // computing / ada / ada
    p.status = "active"                       // new -> active
    p.bio = "  likes math  "; println("[${p.bio}]") // [likes math]
    val s = CountingSet<Int>(); s.add(1); s.add(1)
    println("${s.size} ${s.attempts}")        // 1 2
}

Remember

  • Interface by obj generates forwarding methods; override only what changes
  • by lazy computes once on first access, thread-safe by default
  • observable/vetoable react to or veto property changes
  • Custom delegates implement getValue/setValue

Check yourself

  1. When is the initializer of val x by lazy { compute() } executed?

Sign in to answer, see the explanations, and get this lesson back as a quick review later.

Official docs: kotlinlang.org/docs/delegated-properties.html

Builds on

Unlocks

Nothing builds on this yet.