Kotlin Property Delegates You’re Probably Not Using: observable and vetoable

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.

Code on a dark monitor screen
Property delegates let you intercept reads and writes without boilerplate setters.

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.

Developer working at a desk with code on screen
With observable delegates, your properties carry their own reaction logic.

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.

Scroll to Top