Delegated Properties

lazy, observable, vetoable, map-backed properties, and how to write your own property delegate.

by lazy observable vetoable Map-backed Custom delegate
๐ŸŽ

What Property Delegation Is

Classes & 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

Null 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.

Deferred, cached initialization by lazy
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 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.

Reacting to every assignment Delegates.observable
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 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.

Rejecting an invalid assignment Delegates.vetoable
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
๐Ÿ’ก
Reach for 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

A 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.

Typed access over an untyped map Map delegate
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

A 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.

A delegate that logs every access Custom delegate
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
โ„น๏ธ
The 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

Delegate Syntax Notes
Lazy initializationval x by lazy { ... }Runs once, on first read; thread-safe by default
React to changevar x by Delegates.observable(init) { p, old, new -> }Callback runs after the assignment takes effect
Reject a changevar x by Delegates.vetoable(init) { p, old, new -> }Callback returns Boolean; false keeps the old value
Map-backed propertyval x: T by mapReads map[propertyName], cast to T
Custom read-only delegateoperator fun getValue(thisRef, property): TRequired for a val delegate
Custom mutable delegateoperator fun setValue(thisRef, property, value: T)Required in addition, for a var delegate
Property metadataproperty: KProperty<*>property.name gives the delegate the property's name