How to combine kotlin delegated property: observable, vetoable, and "by map"?
Asked Answered
H

1

5

I'm trying to combine delegates/observable with vetoable (which isn't a problem after looking at the source kotlin.properties.Delegates.kt), but things got hairy when trying to also store the properties in a map.

Or in other words, how to combine these three:

var k1: Int by Delegates.observable(0) { property, oldValue, newValue ->
    println("Hi from k1 observer")
}

var k2:Int by Delegates.vetoable(0) {property, oldValue, newValue ->
    println("Hi from k2 more-than check")
     oldValue > newValue
}

val myMap = mutableMapOf<String, Int>()
var k3 by myMap
Halfbaked answered 15/10, 2018 at 2:55 Comment(0)
U
6

There is no easy way to compose delegates but you can write your own delegate, for example:

inline fun <T> mapVetoObserver(
        map: MutableMap<String, T>,
        absentValue: T,
        crossinline veto: ((property: KProperty<*>, oldValue: T, newValue: T) -> Boolean),
        crossinline observe: ((property: KProperty<*>, oldValue: T, newValue: T) -> Unit))
        : ReadWriteProperty<Any?, T> {

    return object : ReadWriteProperty<Any?, T> {

        // there is no good way to set the map value to some initial value upon delegate
        // construction since the property name is unknown until getValue/setValue are called
        // => absent value rather than initial value

        override fun getValue(thisRef: Any?, property: KProperty<*>): T {
            return map[property.name] ?: absentValue
        }

        override fun setValue(thisRef: Any?, property: KProperty<*>, value: T) {
            val oldValue = getValue(thisRef, property)
            if (!veto(property, oldValue, value)) {
                map[property.name] = value
                observe(property, oldValue, value)
            }
        }
    }
}

and then use it like

val myMap = mutableMapOf<String, Int>()
var k1: Int by mapVetoObserver(myMap, 0,
        { _, oldValue, newValue ->
            (oldValue > newValue).also {
                println("veto: $oldValue => $newValue = $it")
            }
        },
        { property, oldValue, newValue ->
            println("${property.name} changed from $oldValue to $newValue")
        })
var k2: Int by mapVetoObserver(myMap, 42,
        { _, oldValue, newValue ->
            (oldValue > newValue).also {
                println("veto: $oldValue => $newValue = $it")
            }
        },
        { property, oldValue, newValue ->
            println("${property.name} changed from $oldValue to $newValue")
        })

Some example usage:

println("k1: $k1") // k1: 0
println("k2: $k2") // k2: 42
// absentValue isn't in map
println(myMap)     // {}

k1 = 5             // veto: 0 => 5 = false
                   // k1 changed from 0 to 5
println("k1: $k1") // k1: 5
k1 = 3             // veto: 5 => 3 = true
println("k1: $k1") // k1: 5
// changes propagate to the map
println(myMap)     // {k1=5}
// this circumvents veto
myMap["k1"] = 3
println("k1: $k1") // k1: 3
println(myMap)     // {k1=3}

Note: if you want default values in the map, put them there yourself

Unschooled answered 15/10, 2018 at 10:15 Comment(2)
I think that will work! I was curious: would it make more sense to reuse the existing ObservableProperty.beforeChange for veto? Or is that where things get sticky with the cross between "property" and "property via map"?Halfbaked
@BenjaminH exactly, they cache the property value internally and that prevents changes made manually to the map to be visible in the property. If you don't touch the map, you can make it work I guess, e.g. on changed write to the mapUnschooled

© 2022 - 2024 — McMap. All rights reserved.