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.8lazy 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: 5notNull — 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"
}