Post

Delegated properties in Kotlin

Delegated Properties

Properties in Kotlin are fundamental building blocks of everyday software development. They are powerful on their own, and their setters and getters can be customized or, with a few keywords, initialized later or even loaded lazily.

Property delegation allows for even more customization. In a way, it’s somewhat similar to Property Wrappers in Swift (without the projected value and annotation). Both solutions focus on easily adding logic to an existing property.

A simple example

Let’s say we have this class:

1
2
3
4
5
6
class PropertyDelegationExample {

    var number: Int = 0
    var text: String? = null

}

For some reasons, in another part of my code, I want to print to the console every time the number is even and every time the text contains the @ character. Of course, I have many options to do this, but let’s assume for the sake of the example that I want to do it with property delegation. So, let’s create a property delegator that implements a simple observer pattern to help us with this task.

We will need a callback that we can use to listen for value changes:

1
typealias Listener<Value> = (Value) -> Unit

And the property delegator itself, which has two necessary functions, getValue and setValue:

1
2
3
4
class ObserverDelegate {
    operator fun <Value>getValue(thisRef: Any?, property: KProperty<*>): Value {}
    operator fun <Value>setValue(thisRef: Any?, property: KProperty<*>, value: Value) {}
}

If we want to handle more than one type and more than one callback, we will need to store them:

1
2
3
4
5
6
7
8
class ObserverDelegate {

    private val listeners = mutableMapOf<String, MutableList<Listener<*>>>()
    private val values = mutableMapOf<String, Any?>()

    operator fun <Value>getValue(thisRef: Any?, property: KProperty<*>): Value {}
    operator fun <Value>setValue(thisRef: Any?, property: KProperty<*>, value: Value) {}
}

And of course, we need a way to add and remove listeners:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
class ObserverDelegate {

    private val listeners = mutableMapOf<String, MutableList<Listener<*>>>()
    private val values = mutableMapOf<String, Any?>()

    operator fun <Value>getValue(thisRef: Any?, property: KProperty<*>): Value {}
    operator fun <Value>setValue(thisRef: Any?, property: KProperty<*>, value: Value) {}

    fun <Value>listen(property: KProperty<Value>, listener: Listener<Value>) {
        var listeners: MutableList<Listener<Value>>? = this.listeners[property.toString()] as MutableList<Listener<Value>>?
        if (listeners == null) {
            listeners = mutableListOf()
        }
        listeners.add(Listener)
        this.listeners[property.toString()] = listeners as MutableList<Listener<*>>
    }

    fun <Value>leave(property: KProperty<Value>, listener: Listener<Value>) {
        this.listeners[property.toString()]?.remove(Listener)
    }
}

Let’s pause for a moment to understand what we’ve created here.

The listen function is generic over the Value type, taking two parameters: a property representation and a callback. The generic constraint on the callback ensures it’s compatible with the property’s type.

We use property.toString() as a key because the KProperty instances passed to getValue/setValue, listen/leave are different even for the same property. We can’t rely on equality or access the property’s receiver directly. It could be more elegant, Using the string representation is practical, and for now just simply good enough.

The core functionality is straightforward: we store callbacks in a list associated with the property key.

The setValue and getValue is also simple:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
@Suppress("UNCHECKED_CAST")
class ObserverDelegate {

    private val listeners = mutableMapOf<String, MutableList<Listener<*>>>()
    private val values = mutableMapOf<String, Any?>()

    operator fun <Value>getValue(thisRef: Any?, property: KProperty<*>): Value {
        return values[property.toString()] as Value
    }

    operator fun <Value>setValue(thisRef: Any?, property: KProperty<*>, value: Value) {
        values[property.toString()] = value
        listeners[property.toString()]?.forEach { Listener ->
            (Listener as Listener<Value>)(value)
        }
    }

    fun <Value>listen(property: KProperty<Value>, listener: Listener<Value>) {
        var listeners: MutableList<Listener<Value>>? = this.listeners[property.toString()] as MutableList<Listener<Value>>?
        if (listeners == null) {
            listeners = mutableListOf()
        }
        listeners.add(Listener)
        this.listeners[property.toString()] = listeners as MutableList<Listener<*>>
    }

    fun <Value>leave(property: KProperty<Value>, listener: Listener<Value>) {
        this.listeners[property.toString()]?.remove(Listener)
    }
}

In the getter, we simply retrieve the current value of the property and return it. In the setter, we first set the new value and then iterate over all registered listeners, notifying them of the change by passing the new value.

We’ve added an unchecked cast suppression warning to the class. However, this should be safe because the property type remains consistent, and the callback type is tightly coupled to it, preventing type mismatches.

So let’s use our delegate:

1
2
3
4
5
6
class PropertyDelegationExample {

    val observer = ObserverDelegate()
    var number: Int by observer
    var text: String? by observer
}

and listen to the value changes:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
// ...
val propertyDelegationExample = PropertyDelegationExample()

propertyDelegationExample.observer.listen(propertyDelegationExample::number) {
    if (it % 2 == 0) {
        println("Number is even: $it")
    }
}
propertyDelegationExample.observer.listen(propertyDelegationExample::text) {
    if (it != null && it.contains("@")) {
        println("Text contains @: $it")
    }
}

propertyDelegationExample.number = 42 // Number is even: 42

propertyDelegationExample.text = null

propertyDelegationExample.text = "test@test" // Text contains @: test@test

// ...

Conclusion

I know, this is just a simple example, but hopefully, it’s enough to spark some creative thinking. There are often more solutions available than you might initially realize, and finding the best fit for your specific needs is the key to good code quality and personal growth as a developer.

This post is licensed under CC BY 4.0 by the author.