What Property Delegation Is
by ยท getValue ยท setValueClasses & Objects shows custom get()/set() accessors written by hand for one property at a time. Property delegation generalizes that pattern: instead of writing the accessor logic inline, you hand the property off to a separate delegate object via by, and that object's getValue/setValue functions are what actually run on every read and write. The benefit is reuse โ write "lazily initialized," "notify on change," or "validate before assigning" once, as a delegate, and apply it to any number of properties across any number of classes.
var name: String by SomeDelegate() Reading `name` โ SomeDelegate.getValue(thisRef, property) Writing `name` = x โ SomeDelegate.setValue(thisRef, property, x) The delegate object, not the enclosing class, owns the actual storage and the logic for what happens on get/set.
lazy
computed once ยท thread-safe by defaultNull Safety introduces lazy as the counterpart to lateinit. As a delegate specifically: lazy { } returns a Lazy<T> instance, and by wires that instance's getValue to run the initializer block on first access, caching the result for every access after that.
class UserProfile(val userId: Int) { // The block runs once, on the first read of `avatar` โ not at // construction time, and not again on subsequent reads val avatar: Bitmap by lazy { println("loading avatar for user $userId") loadAvatarFromDisk(userId) } } val profile = UserProfile(1) println("profile created") profile.avatar // "loading avatar for user 1", then loads profile.avatar // nothing printed: cached result returned directly // Thread-safety mode is a constructor parameter when it matters val singleThreadOnly by lazy(LazyThreadSafetyMode.NONE) { expensiveInit() }
observable
Delegates.observable ยท react to changeDelegates.observable runs a callback after every assignment to the property, receiving both the old and new value. It's the delegate-based equivalent of a custom setter that also notifies listeners โ useful for UI state, logging, or any place a change needs to trigger a side effect elsewhere.
import kotlin.properties.Delegates class ThemeSettings { var darkMode: Boolean by Delegates.observable(false) { property, old, new -> println("${property.name} changed: $old -> $new") applyThemeChange(new) } } val settings = ThemeSettings() settings.darkMode = true // darkMode changed: false -> true settings.darkMode = false // darkMode changed: true -> false
vetoable
Delegates.vetoable ยท reject the assignmentDelegates.vetoable is observable's stricter sibling: the callback runs before the assignment takes effect, and its boolean return value decides whether the new value is actually accepted. Returning false silently keeps the old value โ the property never changes, no exception thrown.
import kotlin.properties.Delegates class Thermostat { // The lambda returns true to accept the new value, false to reject it var targetTemp: Int by Delegates.vetoable(20) { _, _, new -> new in 10..32 // only accept a plausible temperature range } } val thermostat = Thermostat() thermostat.targetTemp = 22 println(thermostat.targetTemp) // 22: accepted thermostat.targetTemp = 200 println(thermostat.targetTemp) // still 22: rejected, no exception, no change
vetoable when an invalid assignment should be silently ignored rather than treated as an error โ think of it as the delegate-based cousin of validation in a custom setter, but positioned specifically for "reject and keep the old value" rather than "throw." If invalid input should be a hard failure instead, a custom setter with require() (see Errors & Result) is the more direct tool.Map-Backed Properties
Map<String, Any?> ยท dynamic-ish dataA Map<String, Any?> can itself act as a property delegate โ reading a property looks up its name as a key in the map, writing does the equivalent insert. This is a natural fit for data that arrives as a loosely-typed map (parsed JSON, request parameters) but that you'd rather access through named, typed properties than raw string-keyed lookups scattered through the code.
class User(val raw: Map<String, Any?>) { // Each property reads its own name as the map key โ // "name" and "age" below correspond to raw["name"] / raw["age"] val name: String by raw val age: Int by raw } val user = User(mapOf("name" to "Ada", "age" to 36)) println(user.name) // "Ada" โ read from raw["name"], cast to String println(user.age) // 36 โ read from raw["age"], cast to Int // A missing key throws NoSuchElementException at the point of access, // not at construction โ the same deferred-checking trade-off lazy has
Writing a Custom Delegate
getValue ยท setValue ยท operator funA type becomes usable after by simply by providing getValue (for a read-only val) or both getValue and setValue (for a mutable var), each marked operator โ the same convention that powers operator overloading in Functions & Lambdas. No interface needs implementing; the compiler checks for the right function signatures structurally.
import kotlin.reflect.KProperty class LoggingDelegate<T>(private var value: T) { operator fun getValue(thisRef: Any?, property: KProperty<*>): T { println("reading ${property.name} -> $value") return value } operator fun setValue(thisRef: Any?, property: KProperty<*>, newValue: T) { println("writing ${property.name}: $value -> $newValue") value = newValue } } class Account { var balance: Int by LoggingDelegate(0) } val account = Account() account.balance = 100 // writing balance: 0 -> 100 println(account.balance) // reading balance -> 100 ... then prints 100
KProperty<*> parameter is Kotlin's reflection metadata for the property itself โ its .name is what let the LoggingDelegate above report which property was touched, without the delegate needing to be told the name explicitly at construction time. This is the same mechanism Delegates.observable and vetoable use internally to include the property name in their own callbacks.Quick Reference
Syntax cheat-sheet| Delegate | Syntax | Notes |
|---|---|---|
| Lazy initialization | val x by lazy { ... } | Runs once, on first read; thread-safe by default |
| React to change | var x by Delegates.observable(init) { p, old, new -> } | Callback runs after the assignment takes effect |
| Reject a change | var x by Delegates.vetoable(init) { p, old, new -> } | Callback returns Boolean; false keeps the old value |
| Map-backed property | val x: T by map | Reads map[propertyName], cast to T |
| Custom read-only delegate | operator fun getValue(thisRef, property): T | Required for a val delegate |
| Custom mutable delegate | operator fun setValue(thisRef, property, value: T) | Required in addition, for a var delegate |
| Property metadata | property: KProperty<*> | property.name gives the delegate the property's name |