Search code examples
kotlinlazy-initialization

let `lazy` recalculate when value changed


I would like to have something like Android Compose's remember(key) { init } in plain JVM Kotlin. So some sort of Lazy delegate but on every access it calls a key function and if its return value changed the value is recalculated. Is there a simple standard library functionality for achieving this or do I need to create a custom class?


Solution

  • First of all, there is no easy and straightforward way to solve this. The problem is that we can't observe the value of a regular property or a function just like that, so we don't know when exactly to re-calculate the value.

    There are multiple possible solutions, depending on our needs. One is to provide a way to invalidate the cached value, so create a very similar utility to Lazy, but with additional invalidate() function.

    Another solution is what you suggested: specify some kind of a key which we use to determine if we need to re-calculate. Code to acquire the current key should be very lightweight as it will be invoked with each access to the property. Example implementation:

    fun <K, V> recalculatingLazy(keyProvider: () -> K, valueProvider: (K) -> V) = object : ReadOnlyDelegate<V> {
        private var lastKey: Any? = NotSet
        @Suppress("UNCHECKED_CAST")
        private var lastValue = null as V
    
        override fun getValue(thisRef: Any?, property: KProperty<*>): V {
            val key = keyProvider()
            return if (key == lastKey) {
                lastValue
            } else {
                lastKey = key
                valueProvider(key).also { lastValue = it }
            }
        }
    }
    
    private object NotSet
    
    interface ReadOnlyDelegate<T> {
        operator fun getValue(thisRef: Any?, property: KProperty<*>): T
    }
    

    Usage:

    fun main() {
        var source = 1
        val target by recalculatingLazy({ source }) {
            println("New key: $it, recalculating...")
            it * 2
        }
    
        println(target) // recalculating...
        println(target)
        source = 2
        println(target) // recalculating...
        println(target)
    }
    

    Please note this implementation is not thread safe.

    In practice, such solution is pretty limited, we can use it in very specific cases only as we need a key to identify the change. This kind of problem is more commonly solved by another solution - by creating observable variables. We have a source variable A of a very specific type which allows to observe it for changes. Then a target variable B observes it, so it knows when to re-calculate.

    Over years we improved this concept greatly: we started with a simple Observer Pattern, but it had many drawbacks. Then there are Reactive Streams. Kotlin improved on this concept by utilizing coroutines - it provided flows:

    suspend fun main(): Unit = coroutineScope {
        val source = MutableStateFlow(1)
        val target = source.map {
            println("New key: $it, recalculating...")
            it * 2
        }
    
        launch {
            target.collect { println("Value: $it") }
        }
    
        delay(500)
        source.value = 2
        delay(500)
        source.value = 3
        delay(500)
    }
    

    Please note this is entirely different concept, everything works asynchronously. We can rewrite the example and ask the target variable directly, as in the previous example, but we don't have guarantees on when exactly after setting the source, the target will be updated. This is intentional behavior.