Why Property Delegates Often Go Unnoticed
Most Kotlin developers know lazy — it’s everywhere. You see it in Android ViewModels, dependency injection setups, and library code. But the standard library ships two more property delegates that are surprisingly powerful and almost never discussed: Delegates.observable and Delegates.vetoable. If you’ve been wiring up manual setters or using LiveData just to observe a simple field change, these might be exactly what you’ve been missing.

What Is a Property Delegate?
In Kotlin, a property delegate is an object that handles the get and set operations of a property on your behalf. When you write by something after a property declaration, you’re handing off storage and access control to that delegate object. The compiler generates the glue code so you never see it.
The standard library’s kotlin.properties.Delegates object is home to observable, vetoable, and notNull. We’ll focus on the first two since they’re the most versatile and least used.
Delegates.observable: React to Every Change
Delegates.observable lets you attach a callback that fires every time the property is assigned a new value. The callback receives three arguments: the property metadata, the old value, and the new value.
import kotlin.properties.Delegates
class UserProfile {
var displayName: String by Delegates.observable("Anonymous") { prop, old, new ->
println("${prop.name} changed: '$old' → '$new'")
// Could also post to analytics, update a cache, notify listeners…
}
}
fun main() {
val profile = UserProfile()
profile.displayName = "Alice" // displayName changed: 'Anonymous' → 'Alice'
profile.displayName = "Alice" // fires again — even if value is the same!
profile.displayName = "Bob" // displayName changed: 'Alice' → 'Bob'
}
One subtle point: the callback fires on every assignment, including ones where the new value equals the old value. If you only care about genuine changes, add a guard:
var score: Int by Delegates.observable(0) { _, old, new ->
if (old != new) updateScoreboard(new)
}
Some Use Case: Reacting to State Without Flow/LiveData
Imagine you have a plain data class in a Presenter(whaaat?!?) or non-Jetpack ViewModel, and you want to automatically refresh the UI when a property changes — without pulling in a full reactive framework.
class SearchPresenter(private val view: SearchView) {
var query: String by Delegates.observable("") { _, old, new ->
if (old != new) {
view.showLoading()
performSearch(new)
}
}
var isFiltered: Boolean by Delegates.observable(false) { _, _, new ->
view.updateFilterIcon(active = new)
performSearch(query)
}
private fun performSearch(q: String) { /* … */ }
}
The view just assigns to presenter.query or presenter.isFiltered, and the reactions happen automatically. No manual onQueryChanged() boilerplate, no separate notification calls.
Now, obviously this is just an example and you would use flow for this but there might be places where you could make use of Delegates.observable.

Delegates.vetoable: Block Assignments That Don’t Pass Your Rules
Delegates.vetoable works the same way as observable, except the callback returns a Boolean. If it returns true, the assignment goes through. If it returns false, the assignment is silently rejected and the property keeps its current value.
var age: Int by Delegates.vetoable(0) { _, _, new ->
new >= 0 // reject negative ages
}
fun main() {
age = 25 // accepted
age = -5 // rejected — age stays 25
println(age) // 25
}
This is inline validation with zero extra code. No custom setter, no require statement, no separate validator class.
Vetoable in Practice: Validated Form Fields
Here’s a more realistic example — a form model where each field self-validates:
class RegistrationForm {
// Email must contain '@'
var email: String by Delegates.vetoable("") { _, _, new ->
new.isEmpty() || new.contains('@')
}
// Username: 3-20 alphanumeric chars
var username: String by Delegates.vetoable("") { _, _, new ->
new.isEmpty() || new.matches(Regex("[a-zA-Z0-9]{3,20}"))
}
// Password: at least 8 characters
var password: String by Delegates.vetoable("") { _, _, new ->
new.isEmpty() || new.length >= 8
}
}
fun main() {
val form = RegistrationForm()
form.email = "not-an-email" // rejected — stays ""
form.email = "user@example.com" // accepted
form.username = "ab" // rejected — too short, stays ""
form.username = "alice123" // accepted
println(form.email) // user@example.com
println(form.username) // alice123
}
Combining observable and vetoable
You can’t stack two delegates on the same property natively, but you can compose them by writing a tiny custom delegate that does both jobs. For the common case of “validate then notify,” a simple wrapper covers it:
import kotlin.properties.ReadWriteProperty
import kotlin.reflect.KProperty
fun validatedObservable(
initial: T,
validate: (T) -> Boolean,
onChange: (T, T) -> Unit
): ReadWriteProperty<Any?, T> = object : ReadWriteProperty<Any?, T> {
private var value = initial
override fun getValue(thisRef: Any?, property: KProperty<*>) = value
override fun setValue(thisRef: Any?, property: KProperty<*>, value: T) {
if (validate(value)) {
val old = this.value
this.value = value
if (old != value) onChange(old, value)
}
}
}
// Usage:
var rating: Int by validatedObservable(
initial = 0,
validate = { it in 1..5 },
onChange = { old, new -> submitRatingToServer(new) }
)
When to Reach for These Delegates
Use Delegates.observable when you need side effects (logging, analytics, UI refresh, cache invalidation) tied to a property change without cluttering your class with setter logic. Use Delegates.vetoable when a property has invariants that must always hold — instead of scattering require calls or defensive checks at every call site. Both of them keep the what (the property) separate from the how (the reaction or the rule), which makes your code significantly easier to test and reason about.
Summary
Kotlin’s Delegates.observable and Delegates.vetoable are lightweight, zero-dependency tools that belong in every Android developer’s standard toolkit. They let properties carry their own behaviour — reacting to changes or enforcing constraints — without polluting your class with custom getter/setter boilerplate. Next time you find yourself writing a setter that does more than just assign a value, ask whether a delegate could express that intent more cleanly.
This post was written by a human with the help of Claude, an AI assistant by Anthropic.
