Delegation

Kotlin Intermediate — Delegation
Kotlin Intermediate
Course 2 · Chapter 4 · Delegation

🎭 Delegation

Delegation lets a property (or a class) hand off its actual behavior to a separate helper object, using the by keyword. This chapter covers the built-in delegates you'll use constantly — lazy, observable, and map delegation — then shows how to write your own.

🔑 The by Keyword — What Delegation Means

A delegated property doesn't store its value directly — instead, getting and setting it is handed off to a separate delegate object, which implements the actual get/set logic:

val name: String by SomeDelegate() // ^ property ^ the object that actually handles get()/set()

This is a single, general mechanism — lazy, observable, and map delegation (covered next) are all just pre-built delegate objects from the standard library, not separate language features. Once the pattern clicks, writing a custom delegate later in this chapter is a small step, not a new concept.

💤 by lazy — Lazy Initialization

lazy delays computing a property's value until the first time it's actually read, then caches the result for every read after that:

class Report { val data: String by lazy { println("Computing data...") // only runs once, on first access "Expensive result" } } val report = Report() println("Report created") println(report.data) // "Computing data..." prints HERE, not at construction println(report.data) // no "Computing data..." — cached result reused

This is ideal for expensive setup work that a property might never actually need — parsing a large config file, opening a connection, building a lookup table — paying the cost only if and when it's genuinely used.

Thread Safety by Default

lazy { } defaults to LazyThreadSafetyMode.SYNCHRONIZED — safe if multiple threads might access the property simultaneously, at a small locking cost. Pass lazy(LazyThreadSafetyMode.NONE) { } to skip the lock when you know only one thread will ever touch it.

val Only — Not var

lazy only supports val, since the whole point is "compute once, then never change." A lazily-initialized mutable value uses a different delegate — Delegates.notNull() or a custom delegate — not lazy.

👀 Delegates.observable — Reacting to Changes

Delegates.observable runs a callback every time a var property is reassigned, receiving the old and new values:

import kotlin.properties.Delegates class User { var name: String by Delegates.observable("Unnamed") { property, old, new -> println("${property.name} changed from '$old' to '$new'") } } val user = User() user.name = "Philip" // name changed from 'Unnamed' to 'Philip' user.name = "Phil" // name changed from 'Philip' to 'Phil'

This is a compact way to get "whenever this changes, do X" without manually writing a custom setter — useful for logging, triggering a UI refresh, or keeping a derived value in sync. A close cousin, Delegates.vetoable, works the same way but its callback returns a Boolean deciding whether the change is allowed at all, rejecting it (keeping the old value) if it returns false.

🗺️ Map Delegation

Properties can be delegated directly to entries in a Map, keyed by the property's name — a compact way to represent an object backed by loosely-structured data:

class Config(map: Map<String, Any?>) { val host: String by map val port: Int by map } val config = Config(mapOf("host" to "localhost", "port" to 8080)) println(config.host) // localhost — read from map["host"] println(config.port) // 8080 — read from map["port"]

Each property's name is used as the map key automatically — val host: String by map reads map["host"] under the hood. This is the exact mechanism used to build typed wrappers around loosely-structured data like a parsed JSON object or a set of raw config values, giving properties strong types on top of a plain Map. A MutableMap can be used the same way with var properties, writing back into the map on assignment.

🛠️ Writing a Custom Delegate

Any class can act as a property delegate, as long as it provides getValue (and, for a var, setValue) as operator functions:

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("Setting ${property.name} to $newValue") value = newValue } } class Settings { var volume: Int by LoggingDelegate(50) } val settings = Settings() settings.volume = 75 // Setting volume to 75 println(settings.volume) // Reading volume: 75 → 75

This is exactly what lazy, Delegates.observable, and map delegation already do internally — they're just pre-written classes with getValue/setValue operator functions, following this same shape. Writing a custom one makes sense whenever a piece of get/set behavior (validation, logging, caching, syncing to storage) needs to be reused across several properties or classes.

Kotlin Property Delegation vs Java

BehaviorJava's ApproachKotlin
Lazy initializationManual null-check-then-compute in the getter, or a libraryby lazy { ... }
React to a changeHand-written setter with extra logicby Delegates.observable(...) { }
Reusable get/set behaviorComposition + manual forwarding methodsA single custom delegate class, reused via by
⚠ Class Delegation Is a Different (Related) Feature

Kotlin also has class delegation, using the same by keyword on a class implementing an interface: class Wrapper(base: Base) : Base by base forwards every interface method to base automatically, only requiring overrides for methods that need different behavior. It shares the by keyword and the general "hand off to another object" idea with property delegation, but is a separate mechanism — worth knowing the name exists, even though this chapter's focus is property delegation.

💻 Coding Challenges

Challenge 1: Lazy Initialization

Write a class DataLoader with a property val expensiveData: List<Int> delegated with by lazy { }, which prints "Loading data..." before returning listOf(1, 2, 3, 4, 5). In main(), create the loader, print a message confirming it was created, then access expensiveData twice — printed proof that the loading message only appears once.

Goal: Confirm lazy initialization runs once, on first access, not at construction.

→ Solution

Challenge 2: Observable Property

Write a class ShoppingCart with a var itemCount: Int delegated with Delegates.observable(0) { ... }, printing a message whenever it changes (old value → new value). Update itemCount three times in main() and confirm each change is logged.

Goal: Practice Delegates.observable for reacting to property changes.

→ Solution

Challenge 3: A Custom Range-Clamping Delegate

Write a custom delegate class RangeClamped(private var value: Int, private val range: IntRange) with getValue/setValue operator functions, where setValue clamps any assigned value into range before storing it (e.g. assigning 150 to a 0..100 range stores 100). Use it for a var brightness: Int by RangeClamped(50, 0..100) property and demonstrate it clamping an out-of-range assignment.

Goal: Write a genuinely useful custom delegate, not just log/print behavior.

→ Solution

💡 Recognize the Pattern, Not Just the Keywords

Every delegate in this chapter — built-in or custom — boils down to "something else owns getValue()/setValue()." Once that clicks, spotting when a custom delegate is worth writing becomes easy: any time the same non-trivial get/set logic (validation, clamping, logging, caching) would otherwise be copy-pasted across several properties.

🎯 What's Next

Next chapter: DSL Building — function literals with receiver, and building type-safe DSLs.