πŸš€ Β· Delegated Properties βš™οΈ

5 min read Β· Updated on by

Learn about delegation, part 2 β€” introduction to delegating properties via getValue and setValueReadOnlyProperty and ReadWriteProperty, and a quick mention of lazyobservable, and delegating to different properties.

Delegation can also help with the repetition involved when defining properties and their getters and setters.

A good example is an observable property β€” a property where a callback should be called whenever it is accessed. Using only the tools we have talked about so far, we would implement this something like the following:


//sampleStart
class MutableMapCache<K, O>(val onCacheAccess: (cache: MutableMap<K, O>) -> Unit) {
    private val cache: MutableMap<K, O> = mutableMapOf()
        get() {
            onCacheAccess(field)
            return field
        }
    fun getOrPut(key: K, default: () -> O): O = cache.getOrPut(key, default)
}

val mutableMapCache = MutableMapCache<Int, String> { println("Cache accessed!") }
//sampleEnd
fun main() {
    val poem = """
        Kotlin, the architect of code's cathedral,
        With extension functions, it's exceptional.
        From arches to domes, a structure so grand,
        In the world of languages, it takes a stand!
    """.trimIndent()
    println(poem)
}

However, we can see that defining a getter in this fashion is a pattern in its own right, and we would like to extract it, and possibly reuse it.

This is precisely what delegated properties are for:


//sampleStart
import kotlin.reflect.KProperty

class MutableMapCache<K, O>(onCacheAccess: (cache: MutableMap<K, O>) -> Unit) {
    private val cache: MutableMap<K, O> by ObservableProp(mutableMapOf(), onCacheAccess)
    fun getOrPut(key: K, default: () -> O): O = cache.getOrPut(key, default)
}

class ObservableProp<T, V>(private val value: V, val onAccess: (V) -> Unit) {
    operator fun getValue(thisRef: T, property: KProperty<*>): V {
        onAccess(value)
        return value
    }
}

val mutableMapCache = MutableMapCache<Int, String> { println("Cache accessed!") }
//sampleEnd
fun main() {
    val poem = """
        In the coding voyage, Kotlin's the captain,
        With extension functions, it's never naptime.
        From waves to horizons, a journey so wide,
        In the world of development, it's the tide!
    """.trimIndent()
    println(poem)
}

You can see that this allows us to implement observable properties in a completely general way, which can then be applied to any property. Again, as with delegating interface implementations, the β€œthing” that we delegate to must be an instance of some class (in this case, we constructed it in place).

A couple of other things need to be unpacked here:

The delegate class provides the implementations for the getter (and possibly setter, see bellow) β€” as such, the property that is being delegated must be synthetic (no backing field). Any data persisting must happen in the delegate β€” in the example, we passed the initial value mutableMapOf() directly into the delegate, as opposed to assigning it.

The delegate class does not need to implement any interface, onlyΒ operator getValue()Β for getters andΒ operator setValue()Β for setters (we'll discussΒ operatorsΒ in a future lesson):

  • getValue receives the property owner as the first parameter, and the KProperty<*> instance corresponding to the property as the second one. The getValue method must return a value of the type of the delegated property.
  • setValue receives the same arguments as getValue, along with a third one, which is the value being set. It returns Unit.

As mentioned in the previous point, implementing an interface is not necessary. However, as a convenience, ReadOnlyProperty and ReadWriteProperty are available as templates, so you don’t have to remember the signatures.

Here’s the same example, but for a var equivalent, where we also observe mutations and take advantage of ReadWriteProperty:


//sampleStart
import kotlin.properties.ReadWriteProperty
import kotlin.reflect.KProperty

class MutableMapCache<K, O>(onCacheAccess: (cache: MutableMap<K, O>) -> Unit) {
    private var cache: MutableMap<K, O> by ObservableProperty(mutableMapOf(), onCacheAccess)
    fun getOrPut(key: K, default: () -> O): O = cache.getOrPut(key, default)
    fun resetCache() {
        cache = mutableMapOf()
    }
}

class ObservableProperty<T, V>(
    private var value: V,
    val onAccessOrMutation: (V) -> Unit
): ReadWriteProperty<T, V> {
    override operator fun getValue(thisRef: T, property: KProperty<*>): V {
        onAccessOrMutation(value)
        return value
    }

    override fun setValue(thisRef: T, property: KProperty<*>, value: V) {
        if(this.value != value) {
            onAccessOrMutation(value)
            this.value = value
        }
    }

}

fun main() {
    val mutableMapCache = MutableMapCache<Int, String> { println("Cache accessed!") }

	mutableMapCache.getOrPut(123) { "Test" } // Prints "Cache accessed!"
	mutableMapCache.resetCache() // Prints "Cache accessed!"
	mutableMapCache.resetCache() // Does nothing
}
//sampleEnd

As it turns out, a similar implementation is already part of the standard library.

Any property can be delegated, not just class properties:


import kotlin.properties.ReadWriteProperty
import kotlin.reflect.KProperty
class ObservableProperty<T, V>(
    private var value: V,
    val onAccessOrMutation: (V) -> Unit
): ReadWriteProperty<T, V> {
    override operator fun getValue(thisRef: T, property: KProperty<*>): V {
        onAccessOrMutation(value)
        return value
    }

    override fun setValue(thisRef: T, property: KProperty<*>, value: V) {
        if(this.value != value) {
            onAccessOrMutation(value)
            this.value = value
        }
    }

}
//sampleStart
fun doStuff() {
    val observable3 by ObservableProperty(3) { println("$it accessed!") }
    // Do stuff
}
//sampleEnd
fun main() {
    val poem = """
        Kotlin, the sculptor in the code's clay,
        With extension properties, it molds the way.
        From shapes to forms, a masterpiece true,
        In the coding gallery, it's the view!
    """.trimIndent()
    println(poem)
}

There’s more to be said about delegated properties, but we’ll leave that to the docs. I strongly recommend that you take a look at the standard delegates β€” some of them are pretty cool, especially lazy and delegating to a different property.

Exercises


import kotlin.properties.ReadOnlyProperty
import kotlin.reflect.KProperty
import org.junit.Assert
import org.junit.Test

class Test {
    @Test fun testSolution() {

        var cacheCallCount = 0
        val cachingRecordFetcher = CachingRecordFetcher(object : Cache<Int, Int> {
            override fun getOrPut(key: Int, default: () -> Int): Int {
                cacheCallCount++
                return default()
            }

        }) { it }

        cachingRecordFetcher.fetch(123)
        Assert.assertTrue("Incorrect implementation of CachingRecordFetcher", cacheCallCount == 0)
    }
}

//sampleStart
interface RecordFetcher<I, T> {
    fun fetch(id: I): T
}

interface Cache<in K, O> {
    fun getOrPut(key: K, default: () -> O): O
}

class NoCache<K, O> : Cache<K, O> {
    override fun getOrPut(key: K, default: () -> O): O = default()
}

class ToggleableCache<K, O>(private val cache: Cache<K, O>) 
	: ReadOnlyProperty<CachingRecordFetcher<K, O>, Cache<K, O>> {
        
    override operator fun getValue(
        thisRef: CachingRecordFetcher<K, O>, 
        property: KProperty<*>
    ): Cache<K, O> =
        if(isCacheToggled()) cache else NoCache()

    private fun isCacheToggled(): Boolean {
        // Check configuration
        return false;
    }
}

/**
 * Use Toggleablecache to implement a mechanism by which the cache can be toggled on/off.
 */
class CachingRecordFetcher<I, T>(
    cache: Cache<I, T>, 
    private val factory: (id: I) -> T
) : RecordFetcher<I, T> {
    
    private val cache: Cache<I, T>
    override fun fetch(id: I): T = cache.getOrPut(id) { factory(id) }
}
//sampleEnd

Solution

Leave a Comment

This site uses Akismet to reduce spam. Learn how your comment data is processed.

The Kotlin Primer