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 aMap<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