Kotlin Built-in Property Delegates

Kotlin's standard library ships with three ready-to-use property delegates: lazy (compute once on first use), observable (run code whenever a value changes), and vetoable (block a change if a condition fails). These cover the most common property behaviors without any custom code.

lazy — Compute Once, Cache Forever

A lazy property computes its value the first time it is accessed, then stores and reuses that value for every subsequent access.

import kotlin.properties.Delegates

class UserProfile(val userId: Int) {
    val fullReport: String by lazy {
        println("Computing report for user $userId...")  // runs only once
        "Report: sales=450, returns=3, rating=4.8"
    }
}

fun main() {
    val user = UserProfile(42)
    println("Profile created")

    println(user.fullReport)   // triggers computation
    println(user.fullReport)   // uses cached value — no recomputation
    println(user.fullReport)   // same cached value
}

Output:

Profile created
Computing report for user 42...
Report: sales=450, returns=3, rating=4.8
Report: sales=450, returns=3, rating=4.8
Report: sales=450, returns=3, rating=4.8

lazy Thread Safety Modes


val data by lazy { expensiveComputation() }
// Default: LazyThreadSafetyMode.SYNCHRONIZED
// Safe for use from multiple threads — only one thread computes the value

val data by lazy(LazyThreadSafetyMode.NONE) { expensiveComputation() }
// Fastest: no synchronization — use only in single-threaded code

val data by lazy(LazyThreadSafetyMode.PUBLICATION) { expensiveComputation() }
// Multiple threads may compute, but only one value is stored

observable — React to Changes

An observable property calls a handler function every time its value changes. You receive the old value and the new value.

import kotlin.properties.Delegates

class StockItem(name: String, price: Double) {
    var name: String by Delegates.observable(name) { prop, old, new ->
        println("Name changed: '$old' → '$new'")
    }

    var price: Double by Delegates.observable(price) { prop, old, new ->
        val change = if (new > old) "▲" else "▼"
        println("Price $change: ₹$old → ₹$new (${prop.name})")
    }
}

fun main() {
    val item = StockItem("Headphones", 1500.0)
    item.name  = "Premium Headphones"
    item.price = 1800.0
    item.price = 1600.0
}

Output:

Name changed: 'Headphones' → 'Premium Headphones'
Price ▲: ₹1500.0 → ₹1800.0 (price)
Price ▼: ₹1800.0 → ₹1600.0 (price)

vetoable — Block Invalid Changes

A vetoable property calls a handler before a change. If the handler returns false, the change is rejected and the old value is kept.

import kotlin.properties.Delegates

class PlayerStats {
    var health: Int by Delegates.vetoable(100) { _, old, new ->
        val allowed = new in 0..100
        if (!allowed) println("Invalid health $new — keeping $old")
        allowed   // return true to accept, false to reject
    }

    var level: Int by Delegates.vetoable(1) { _, old, new ->
        val allowed = new > old   // can only level up, never down
        if (!allowed) println("Cannot set level to $new — currently $old")
        allowed
    }
}

fun main() {
    val player = PlayerStats()
    player.health = 80    // accepted
    player.health = -10   // rejected (out of range)
    player.health = 150   // rejected (out of range)
    println("Health: ${player.health}")

    player.level = 5      // accepted
    player.level = 3      // rejected (cannot decrease)
    println("Level: ${player.level}")
}

Output:

Invalid health -10 — keeping 80
Invalid health 150 — keeping 80
Health: 80
Cannot set level to 3 — currently 5
Level: 5

notNull — Lateinit for Primitives

Delegates.notNull() works like lateinit var but supports primitive types like Int and Double:

import kotlin.properties.Delegates

class Config {
    var maxRetries: Int by Delegates.notNull()
    var timeoutMs: Long by Delegates.notNull()
}

fun main() {
    val config = Config()
    config.maxRetries = 3
    config.timeoutMs  = 5000L
    println("Retries: ${config.maxRetries}, Timeout: ${config.timeoutMs}ms")

    // val c2 = Config()
    // println(c2.maxRetries)   // throws: Property maxRetries should be initialized before get
}

Practical Example: Form Validator

import kotlin.properties.Delegates

class RegistrationForm {
    var username: String by Delegates.observable("") { _, _, new ->
        println("Username set to '$new' — ${if (new.length >= 3) "valid" else "too short"}")
    }

    var age: Int by Delegates.vetoable(0) { _, old, new ->
        val ok = new in 18..100
        if (!ok) println("Age $new rejected — must be 18–100")
        ok
    }

    var email: String by Delegates.observable("") { _, _, new ->
        val valid = new.contains("@") && new.contains(".")
        println("Email '$new' — ${if (valid) "valid" else "invalid format"}")
    }
}

fun main() {
    val form = RegistrationForm()
    form.username = "Al"
    form.username = "Alice"
    form.age  = 15
    form.age  = 28
    form.email = "notanemail"
    form.email = "alice@example.com"
}

Leave a Comment

Your email address will not be published. Required fields are marked *