Use Delegates
Problem
Implementing common property patterns like lazy initialization, observable properties, or validation requires boilerplate code. Kotlin’s property delegation mechanism allows you to extract these patterns into reusable delegates, reducing code duplication and improving maintainability.
This guide shows how to use built-in delegates and create custom ones.
Lazy Delegate
Basic Lazy Initialization
Initialize properties on first access using lazy.
// ✅ Lazy initialization
class DatabaseService {
val connection: Connection by lazy {
println("Establishing connection...")
Database.connect() // Expensive operation
}
}
val service = DatabaseService()
println("Service created") // Connection NOT initialized yet
println(service.connection) // NOW connection is initialized
println(service.connection) // Uses cached valueOutput:
Service created
Establishing connection...
Connection@12345
Connection@12345How it works: Lambda executes on first access, result cached for subsequent accesses.
Lazy Thread Safety Modes
Control thread safety behavior of lazy initialization.
// ✅ SYNCHRONIZED (default) - thread-safe with lock
val heavyResource: Resource by lazy {
Resource.load()
}
// ✅ PUBLICATION - multiple threads may initialize, first wins
val cachedData: Data by lazy(LazyThreadSafetyMode.PUBLICATION) {
Data.fetch()
}
// ✅ NONE - no thread safety, fastest
val singleThreadedCache: Cache by lazy(LazyThreadSafetyMode.NONE) {
Cache.create() // Only use in single-threaded contexts
}Thread safety modes:
SYNCHRONIZED(default): Thread-safe with double-check lockingPUBLICATION: Multiple initializations possible, first result winsNONE: No synchronization, fastest but not thread-safe
Lazy with Dependencies
Lazy properties can depend on other properties.
// ✅ Chained lazy initialization
class Configuration {
val environment: String by lazy {
System.getenv("ENV") ?: "development"
}
val databaseUrl: String by lazy {
when (environment) {
"production" -> "jdbc:postgresql://prod-db/app"
"staging" -> "jdbc:postgresql://stage-db/app"
else -> "jdbc:h2:mem:test"
}
}
val connection: Connection by lazy {
DriverManager.getConnection(databaseUrl)
}
}
val config = Configuration()
// All properties initialized on first access in correct order
println(config.connection)Pattern: Dependencies resolved automatically during initialization.
Observable Delegate
Tracking Property Changes
React to property changes with observable.
// ✅ Observable property
import kotlin.properties.Delegates
class User {
var name: String by Delegates.observable("") { property, oldValue, newValue ->
println("${property.name} changed from '$oldValue' to '$newValue'")
}
}
val user = User()
user.name = "Alice" // Prints: name changed from '' to 'Alice'
user.name = "Bob" // Prints: name changed from 'Alice' to 'Bob'Use case: Logging, UI updates, validation triggers.
Multiple Observers
Implement multi-cast observers for complex scenarios.
// ✅ Multiple observers
class ViewModel {
private val listeners = mutableListOf<(String) -> Unit>()
var status: String by Delegates.observable("idle") { _, _, newValue ->
listeners.forEach { it(newValue) }
}
fun addListener(listener: (String) -> Unit) {
listeners.add(listener)
}
}
val viewModel = ViewModel()
viewModel.addListener { status -> println("UI: Status is $status") }
viewModel.addListener { status -> println("Log: Status changed to $status") }
viewModel.status = "loading"
// UI: Status is loading
// Log: Status changed to loadingObservable with State Management
Use observable for reactive state management.
// ✅ State management with observable
data class AppState(
val isLoading: Boolean = false,
val error: String? = null,
val data: List<String> = emptyList()
)
class Store {
var state: AppState by Delegates.observable(AppState()) { _, oldState, newState ->
if (oldState != newState) {
notifySubscribers(newState)
}
}
private val subscribers = mutableListOf<(AppState) -> Unit>()
fun subscribe(subscriber: (AppState) -> Unit) {
subscribers.add(subscriber)
}
private fun notifySubscribers(state: AppState) {
subscribers.forEach { it(state) }
}
}
val store = Store()
store.subscribe { state -> println("State updated: $state") }
store.state = store.state.copy(isLoading = true)Vetoable Delegate
Validation Before Assignment
Use vetoable to validate or reject property changes.
// ✅ Vetoable property
import kotlin.properties.Delegates
class Account {
var balance: Double by Delegates.vetoable(0.0) { _, _, newValue ->
newValue >= 0.0 // Only allow non-negative balance
}
}
val account = Account()
account.balance = 100.0 // ✅ Accepted
println(account.balance) // 100.0
account.balance = -50.0 // ❌ Rejected
println(account.balance) // 100.0 (unchanged)How it works: Lambda returns true to accept, false to reject change.
Complex Validation
Implement multi-condition validation.
// ✅ Complex validation with vetoable
class User {
var email: String by Delegates.vetoable("") { property, oldValue, newValue ->
when {
newValue.isBlank() -> {
println("Email cannot be blank")
false
}
!newValue.contains("@") -> {
println("Invalid email format")
false
}
newValue.length > 100 -> {
println("Email too long")
false
}
else -> true
}
}
}
val user = User()
user.email = "alice@example.com" // ✅ Accepted
user.email = "invalid" // ❌ Rejected: Invalid email format
user.email = "" // ❌ Rejected: Email cannot be blankVetoable with Side Effects
Combine validation with logging or notifications.
// ✅ Vetoable with side effects
class Temperature {
var celsius: Double by Delegates.vetoable(20.0) { _, oldValue, newValue ->
val isValid = newValue in -273.15..1000.0 // Physical limits
if (!isValid) {
logger.warn("Invalid temperature: $newValue°C")
} else if (newValue > 30.0) {
logger.info("High temperature alert: $newValue°C")
}
isValid
}
}Map Delegate
Storing Properties in Map
Use map as property delegate for dynamic property storage.
// ✅ Properties backed by map
class User(val map: Map<String, Any?>) {
val name: String by map
val age: Int by map
val email: String? by map
}
val user = User(mapOf(
"name" to "Alice",
"age" to 30,
"email" to "alice@example.com"
))
println(user.name) // Alice
println(user.age) // 30
println(user.email) // alice@example.comUse case: JSON parsing, configuration files, dynamic data.
Mutable Map Delegate
Use mutable map for read-write properties.
// ✅ Mutable properties backed by mutable map
class MutableUser(val map: MutableMap<String, Any?>) {
var name: String by map
var age: Int by map
var email: String? by map
}
val user = MutableUser(mutableMapOf(
"name" to "Bob",
"age" to 25
))
println(user.email) // null
user.email = "bob@example.com" // Modifies backing map
println(user.email) // bob@example.com
println(user.map) // {name=Bob, age=25, email=bob@example.com}Default Values with Map
Provide defaults for missing map keys.
// ✅ Map with defaults
class Config(private val map: Map<String, Any?>) {
val timeout: Int by map.withDefault { 30 }
val retries: Int by map.withDefault { 3 }
val debug: Boolean by map.withDefault { false }
}
val config = Config(mapOf("timeout" to 60))
println(config.timeout) // 60 (from map)
println(config.retries) // 3 (default)
println(config.debug) // false (default)Custom Delegates
Implementing ReadOnlyProperty
Create read-only delegate implementing ReadOnlyProperty interface.
// ✅ Custom read-only delegate
import kotlin.reflect.KProperty
class Uppercase {
operator fun getValue(thisRef: Any?, property: KProperty<*>): String {
return property.name.uppercase()
}
}
class Example {
val greeting: String by Uppercase()
val message: String by Uppercase()
}
val example = Example()
println(example.greeting) // GREETING
println(example.message) // MESSAGEInterface: getValue(thisRef: Any?, property: KProperty<*>): T
Implementing ReadWriteProperty
Create read-write delegate implementing ReadWriteProperty interface.
// ✅ Custom read-write delegate
import kotlin.properties.ReadWriteProperty
import kotlin.reflect.KProperty
class Trimmed : ReadWriteProperty<Any?, String> {
private var value: String = ""
override fun getValue(thisRef: Any?, property: KProperty<*>): String {
return value
}
override fun setValue(thisRef: Any?, property: KProperty<*>, value: String) {
this.value = value.trim()
}
}
class Form {
var username: String by Trimmed()
var email: String by Trimmed()
}
val form = Form()
form.username = " alice "
form.email = " alice@example.com "
println("'${form.username}'") // 'alice'
println("'${form.email}'") // 'alice@example.com'Parameterized Delegates
Create delegates that accept parameters.
// ✅ Delegate with parameters
class RangeValidator(
private val min: Int,
private val max: Int
) : ReadWriteProperty<Any?, Int> {
private var value: Int = min
override fun getValue(thisRef: Any?, property: KProperty<*>): Int {
return value
}
override fun setValue(thisRef: Any?, property: KProperty<*>, value: Int) {
require(value in min..max) {
"${property.name} must be between $min and $max, got $value"
}
this.value = value
}
}
class Player {
var health: Int by RangeValidator(0, 100)
var level: Int by RangeValidator(1, 50)
}
val player = Player()
player.health = 75 // ✅ Valid
player.level = 10 // ✅ Valid
// player.health = 150 // ❌ Throws exceptionDelegate Factory Functions
Create factory functions for common delegate patterns.
// ✅ Delegate factory function
fun <T> cached(initializer: () -> T) = lazy(initializer)
fun <T> validated(
initial: T,
validator: (T) -> Boolean
): ReadWriteProperty<Any?, T> {
return object : ReadWriteProperty<Any?, T> {
private var value: T = initial
override fun getValue(thisRef: Any?, property: KProperty<*>): T = value
override fun setValue(thisRef: Any?, property: KProperty<*>, value: T) {
require(validator(value)) { "Validation failed for ${property.name}" }
this.value = value
}
}
}
class Config {
val apiUrl: String by cached { "https://api.example.com" }
var timeout: Int by validated(30) { it in 1..300 }
}Advanced Patterns
Contextual Delegates
Delegates that use containing class context.
// ✅ Delegate with access to containing class
class LoggedProperty<T>(private val initialValue: T) : ReadWriteProperty<Any, T> {
private var value: T = initialValue
override fun getValue(thisRef: Any, property: KProperty<*>): T {
println("[${thisRef::class.simpleName}] Reading ${property.name}: $value")
return value
}
override fun setValue(thisRef: Any, property: KProperty<*>, value: T) {
println("[${thisRef::class.simpleName}] Setting ${property.name}: $value")
this.value = value
}
}
class User {
var name: String by LoggedProperty("Unknown")
}
val user = User()
user.name = "Alice"
// [User] Setting name: Alice
println(user.name)
// [User] Reading name: AliceComposed Delegates
Combine multiple delegate behaviors.
// ✅ Composed delegate
class ValidatedObservable<T>(
initialValue: T,
private val validator: (T) -> Boolean,
private val observer: (T, T) -> Unit
) : ReadWriteProperty<Any?, T> {
private var value: T = initialValue
override fun getValue(thisRef: Any?, property: KProperty<*>): T = value
override fun setValue(thisRef: Any?, property: KProperty<*>, newValue: T) {
require(validator(newValue)) { "Validation failed" }
val oldValue = value
value = newValue
observer(oldValue, newValue)
}
}
fun <T> validatedObservable(
initialValue: T,
validator: (T) -> Boolean,
observer: (T, T) -> Unit
) = ValidatedObservable(initialValue, validator, observer)
class Account {
var balance: Double by validatedObservable(
initialValue = 0.0,
validator = { it >= 0.0 },
observer = { old, new -> println("Balance changed: $old → $new") }
)
}Delegate Providers
Create delegates dynamically using provideDelegate.
// ✅ Delegate provider
import kotlin.properties.PropertyDelegateProvider
import kotlin.properties.ReadWriteProperty
import kotlin.reflect.KProperty
class LogLevel(private val level: String) {
operator fun provideDelegate(
thisRef: Any?,
property: KProperty<*>
): ReadWriteProperty<Any?, String> {
println("Creating delegate for ${property.name} with level $level")
return object : ReadWriteProperty<Any?, String> {
private var value: String = ""
override fun getValue(thisRef: Any?, property: KProperty<*>): String {
return value
}
override fun setValue(thisRef: Any?, property: KProperty<*>, value: String) {
println("[$level] ${property.name} = $value")
this.value = value
}
}
}
}
class Logger {
var info: String by LogLevel("INFO")
var error: String by LogLevel("ERROR")
}
// Output when class is initialized:
// Creating delegate for info with level INFO
// Creating delegate for error with level ERRORCommon Pitfalls
Thread Safety with Lazy
// ❌ NONE mode in multi-threaded context
class Cache {
val data: Data by lazy(LazyThreadSafetyMode.NONE) {
Data.load() // ❌ Not thread-safe if accessed concurrently
}
}
// ✅ Use SYNCHRONIZED (default) for multi-threaded
class Cache {
val data: Data by lazy { // SYNCHRONIZED mode by default
Data.load()
}
}Why problematic: NONE mode can cause race conditions in concurrent scenarios.
Observable Not Called on Same Value
// ❌ Observable only fires when value changes
var count: Int by Delegates.observable(0) { _, old, new ->
println("Changed: $old → $new")
}
count = 5 // Prints: Changed: 0 → 5
count = 5 // Does NOT print (same value)Workaround: Use custom delegate if you need to observe all assignments.
Map Delegate Type Mismatch
// ❌ Runtime exception on type mismatch
class Config(map: Map<String, Any?>) {
val timeout: Int by map
}
val config = Config(mapOf("timeout" to "30")) // String, not Int
// ❌ Throws ClassCastException at runtimeSolution: Validate map contents or use type-safe parsing.
Forgetting Delegation Syntax
// ❌ Missing 'by' keyword
class User {
val name: String = Delegates.observable("") // Wrong!
}
// ✅ Correct delegation syntax
class User {
var name: String by Delegates.observable("")
}Variations
Delegate for Dependency Injection
Use delegates for service location.
// ✅ Delegate-based dependency injection
object ServiceLocator {
private val services = mutableMapOf<String, Any>()
fun <T> register(name: String, service: T) {
services[name] = service as Any
}
operator fun <T> getValue(thisRef: Any?, property: KProperty<*>): T {
@Suppress("UNCHECKED_CAST")
return services[property.name] as T
}
}
class MyApp {
val database: Database by ServiceLocator
val apiClient: ApiClient by ServiceLocator
}
// Setup
ServiceLocator.register("database", DatabaseImpl())
ServiceLocator.register("apiClient", ApiClientImpl())
val app = MyApp()Reset-able Lazy
Create lazy delegate that can be reset.
// ✅ Reset-able lazy delegate
class ResettableLazy<T>(private val initializer: () -> T) : ReadWriteProperty<Any?, T> {
private var value: T? = null
override fun getValue(thisRef: Any?, property: KProperty<*>): T {
return value ?: initializer().also { value = it }
}
override fun setValue(thisRef: Any?, property: KProperty<*>, value: T) {
this.value = null // Reset on any assignment
}
fun reset() {
value = null
}
}
fun <T> resettableLazy(initializer: () -> T) = ResettableLazy(initializer)
class Cache {
var data: List<String> by resettableLazy {
println("Loading data...")
listOf("Item1", "Item2", "Item3")
}
}
val cache = Cache()
println(cache.data) // Loading data... [Item1, Item2, Item3]
cache.data = listOf() // Reset
println(cache.data) // Loading data... [Item1, Item2, Item3]Related Patterns
Learn more:
- Beginner Tutorial - Delegates - Delegation basics
- Intermediate Tutorial - Advanced Delegates - Complex patterns
- Custom Delegates - Advanced delegate creation
Cookbook recipes:
- Property Delegation - Quick reference
- Observable Patterns - Reactive programming
- Validation Patterns - Input validation