In the previous tutorial, you learned about error handling. Now let’s learn about delegation. Delegation is a design pattern where an object hands off work to another object. Kotlin has built-in support for delegation using the by keyword.
In this tutorial, you will learn:
by lazy— lazy initializationDelegates.observable— react to changesDelegates.vetoable— reject invalid changes- Map delegation — properties from a map
- Custom delegates — build your own
- Class delegation — delegate interface implementation
by lazy
by lazy creates a property that is computed only once, the first time you access it. After that, the cached value is returned. This is useful for expensive initialization.
class DatabaseConnection {
val connection: String by lazy {
println("Connecting to database...")
"Connected to PostgreSQL"
}
}
val db = DatabaseConnection()
println("Object created")
println(db.connection) // Triggers lazy init, prints "Connecting..."
println(db.connection) // Uses cached value, no "Connecting..." printed
Output:
Object created
Connecting to database...
Connected to PostgreSQL
Connected to PostgreSQL
The lambda runs only on the first access. Every access after that returns the same value.
Thread Safety
By default, lazy is thread-safe. It uses synchronization to ensure the value is computed only once, even if multiple threads access it at the same time.
If you know the property will only be accessed from one thread, you can use LazyThreadSafetyMode.NONE for better performance:
val fastLazy: String by lazy(LazyThreadSafetyMode.NONE) {
"Computed value"
}
When to Use lazy
- Expensive computations (database connections, file loading)
- Properties that might never be accessed
- Properties that depend on other properties being set first
- Singleton-style initialization
val expensiveValue: String by lazy {
println("Computing...")
"Result of expensive computation"
}
Delegates.observable
Delegates.observable fires a callback every time the value changes. You get the old value and the new value.
import kotlin.properties.Delegates
class UserSettings {
var theme: String by Delegates.observable("Light") { _, old, new ->
println("Theme changed: $old -> $new")
}
var fontSize: Int by Delegates.observable(14) { _, old, new ->
println("Font size changed: $old -> $new")
}
}
val settings = UserSettings()
settings.theme = "Dark" // Theme changed: Light -> Dark
settings.theme = "Blue" // Theme changed: Dark -> Blue
settings.fontSize = 18 // Font size changed: 14 -> 18
The first parameter _ is the property reference. We do not need it in most cases.
The callback receives three parameters:
property— the property being changed (usually ignored with_)old— the previous valuenew— the new value
Observable is useful for:
- Logging property changes
- Updating the UI when data changes
- Syncing state between components
- Tracking what changed in a form
Delegates.vetoable
Delegates.vetoable fires a callback before the value changes. Return true to accept the change, false to reject it.
class Player {
// Health cannot go below 0
var health: Int by Delegates.vetoable(100) { _, _, new ->
new >= 0
}
// Score can only increase
var score: Int by Delegates.vetoable(0) { _, old, new ->
new >= old
}
// Name must not be blank
var name: String by Delegates.vetoable("Player") { _, _, new ->
new.isNotBlank()
}
}
Usage:
val player = Player()
player.health = 80
println(player.health) // 80
player.health = -10 // Rejected!
println(player.health) // Still 80
player.score = 100
println(player.score) // 100
player.score = 50 // Rejected — can only increase
println(player.score) // Still 100
player.name = "Alex"
println(player.name) // Alex
player.name = "" // Rejected — blank
println(player.name) // Still Alex
Vetoable is useful for:
- Validation rules on properties
- Preventing invalid state changes
- Range-restricted values
Map Delegation
You can delegate properties to a Map. The property name becomes the map key. This is useful for parsing JSON or configuration data.
Read-Only Map
class UserFromMap(map: Map<String, Any?>) {
val name: String by map
val age: Int by map
val email: String by map
}
val userData = mapOf(
"name" to "Alex",
"age" to 25,
"email" to "alex@mail.com"
)
val user = UserFromMap(userData)
println(user.name) // Alex
println(user.age) // 25
println(user.email) // alex@mail.com
Mutable Map
With a mutable map, you can also write values:
class MutableConfig(map: MutableMap<String, Any?>) {
var host: String by map
var port: Int by map
var debug: Boolean by map
}
val configData = mutableMapOf<String, Any?>(
"host" to "localhost",
"port" to 8080,
"debug" to false
)
val config = MutableConfig(configData)
println(config.host) // localhost
config.host = "api.example.com"
println(config.host) // api.example.com
println(configData["host"]) // api.example.com — map updated too!
Changing the property updates the map, and vice versa. They share the same data.
Custom Delegates
Create your own delegate by implementing ReadWriteProperty. You need getValue() and setValue().
TrimmedString — Always Trims Whitespace
import kotlin.properties.ReadWriteProperty
import kotlin.reflect.KProperty
class TrimmedString(private var value: String = "")
: ReadWriteProperty<Any?, String> {
override fun getValue(thisRef: Any?, property: KProperty<*>): String {
return value
}
override fun setValue(thisRef: Any?, property: KProperty<*>, value: String) {
this.value = value.trim()
}
}
ClampedInt — Keeps Value in a Range
class ClampedInt(
private var value: Int,
private val min: Int,
private val max: Int
) : ReadWriteProperty<Any?, Int> {
override fun getValue(thisRef: Any?, property: KProperty<*>): Int = value
override fun setValue(thisRef: Any?, property: KProperty<*>, value: Int) {
this.value = value.coerceIn(min, max)
}
}
LoggedProperty — Logs Every Access
class LoggedProperty<T>(private var value: T)
: ReadWriteProperty<Any?, T> {
override fun getValue(thisRef: Any?, property: KProperty<*>): T {
println("Reading ${property.name}: $value")
return value
}
override fun setValue(thisRef: Any?, property: KProperty<*>, value: T) {
println("Writing ${property.name}: ${this.value} -> $value")
this.value = value
}
}
Using Custom Delegates
class FormData {
var username: String by TrimmedString()
var bio: String by TrimmedString()
var volume: Int by ClampedInt(50, 0, 100)
var brightness: Int by ClampedInt(75, 0, 100)
}
val form = FormData()
form.username = " Alex "
println(form.username) // "Alex" — trimmed!
form.volume = 150
println(form.volume) // 100 — clamped to max
form.volume = -10
println(form.volume) // 0 — clamped to min
Class Delegation
Class delegation lets a class delegate interface implementation to another object. The compiler generates all the forwarding methods for you.
Basic Class Delegation
interface Logger {
fun log(message: String)
fun error(message: String)
}
class ConsoleLogger : Logger {
override fun log(message: String) = println("[LOG] $message")
override fun error(message: String) = println("[ERROR] $message")
}
class Service(logger: Logger) : Logger by logger {
fun doWork() {
log("Starting work...")
// ... do something
log("Work completed")
}
}
val service = Service(ConsoleLogger())
service.doWork()
// [LOG] Starting work...
// [LOG] Work completed
The Service class implements Logger by delegating all calls to the logger object. You do not need to write override fun log(...) — the compiler does it.
Override Specific Methods
You can still override specific methods while delegating the rest:
class PrefixedLogger(private val prefix: String, logger: Logger)
: Logger by logger {
override fun log(message: String) {
println("[$prefix] $message") // Custom implementation
}
// error() is still delegated to the original logger
}
val prefixed = PrefixedLogger("APP", ConsoleLogger())
prefixed.log("Hello") // [APP] Hello — uses override
prefixed.error("Oops") // [ERROR] Oops — uses delegate
Multiple Delegation
A class can delegate multiple interfaces:
interface Cache {
fun get(key: String): String?
fun put(key: String, value: String)
}
class InMemoryCache : Cache {
private val store = mutableMapOf<String, String>()
override fun get(key: String): String? = store[key]
override fun put(key: String, value: String) { store[key] = value }
}
class CachedService(
logger: Logger,
cache: Cache
) : Logger by logger, Cache by cache {
fun fetchData(key: String): String {
val cached = get(key)
if (cached != null) {
log("Cache hit: $key")
return cached
}
log("Cache miss: $key")
val data = "Data for $key"
put(key, data)
return data
}
}
Usage:
val service = CachedService(ConsoleLogger(), InMemoryCache())
println(service.fetchData("users")) // Cache miss, returns data
println(service.fetchData("users")) // Cache hit, returns cached data
Delegation vs Inheritance
Consider this example without delegation:
// Without delegation — must override all methods manually
class ServiceWithoutDelegation(private val logger: Logger) : Logger {
override fun log(message: String) = logger.log(message)
override fun error(message: String) = logger.error(message)
fun doWork() {
log("Working...")
}
}
With delegation, you skip all the boilerplate:
// With delegation — compiler generates forwarding methods
class ServiceWithDelegation(logger: Logger) : Logger by logger {
fun doWork() {
log("Working...")
}
}
If Logger had 10 methods, you would need to write 10 override methods without delegation. With delegation, you write zero.
Delegation in Real Projects
Delegation is used widely in Android development and other Kotlin frameworks. For example:
// ViewModel that delegates to a repository
class UserViewModel(
private val repository: Repository<User>
) {
// Can delegate specific behavior
fun getUsers() = repository.getAll()
}
// Adapter pattern with delegation
interface OldApi {
fun fetchData(): String
}
interface NewApi {
fun getData(): String
}
class OldApiImpl : OldApi {
override fun fetchData(): String = "Old data"
}
class ApiAdapter(oldApi: OldApi) : NewApi {
private val delegate = oldApi
override fun getData(): String = delegate.fetchData()
}
Why Use Class Delegation?
- Composition over inheritance — delegate instead of extending
- Decoupling — swap implementations easily
- Less boilerplate — no need to write forwarding methods
Summary
| Delegation Type | Syntax | Use for |
|---|---|---|
| Lazy | by lazy { } | Expensive one-time init |
| Observable | Delegates.observable() | React to changes |
| Vetoable | Delegates.vetoable() | Validate changes |
| Map | by map | Properties from map/JSON |
| Custom | by CustomDelegate() | Custom logic |
| Class | : Interface by impl | Forward interface calls |
Key takeaways:
by lazycomputes a value once and caches itDelegates.observablenotifies you when a value changesDelegates.vetoablelets you reject invalid changes- Map delegation is great for parsing configuration data
- Custom delegates let you add any logic to property access
- Class delegation avoids inheritance and reduces boilerplate
Source Code
You can find the complete source code for this tutorial on GitHub:
What’s Next?
In this tutorial, you learned about lazy initialization, observable and vetoable delegates, map delegation, custom delegates, and class delegation.
In the next tutorial, you will learn about sequences — lazy collections that can improve performance when working with large data sets.
This is part 15 of the Kotlin Tutorial series. Check out Part 14: Error Handling if you missed it. Need a quick reference? See the Kotlin Cheat Sheet.