Observable properties with true listeners instead of a single callback?


#1

The reference for Delegated Properties says they can be useful for

observable properties: listeners get notified about changes to this property

while really you can only have a single callback:

class Example {
    var name1: String by Delegates.observable("<no name1>") { prop, old, new ->
        println("$old -> $new")
    }
}

which is not what I think of when I think “observable” and is not really different from

var name2: String = "<no name2>"
    set(value) {
        println("$field -> $value")
        field = value
    }

Is there a facility in the language/standard library to actually have observable properties? Something in the line of:

name.addListener { old, new -> println(new) }
val myOtherListener = { old: String, new: String -> doSomething(old, new) }
name.addListener(myOtherListener)
name.value = "Bob" // to read/write the actual value
name.removeListener(myOtherListener)

#2

don’t think so.
You can create a delegate yourself.
by calling this::name1.getDelegate() you get the delegated property.
So, if you create an extensionFunction like to which does something like (

fun KProperty<R>.registerListener(listener: (old: R, new: R) -> Unit) {
    (KProperty<R>.getDelegate() as YourDelegationClass).registerListener(listener)
}

you can call it like:

this::name1.registerListener{old, new -> }

Note, the registerListener is added to all the methods.

Maybe another way to do it is creating a manager and adding the listeners to that manager…

challenge

I challenge you to come up with an ObservableManager, where you register the listeners using:

manager.registerListener(this::name1){old, new -> ...}

and you can register using a one-liner, so no additional setup:

val test by  function(defaultValue)

where function can come from anywhere (but every type should be able to call the same function.
bonus-points if you can do it, without using the full reflection.

found it :wink:

solution in steps

The steps:

  1. We specify a typealias for the listeners

    typealias BiConsumer<O> = (O, O) -> Unit
    
  2. We need a class, as we need to save some values. But we also want to let the pojo extend another class, therefor we use the combination of an interface and a class, this is a delegated class (or a trait).

  3. In order to make the class only manage the properties of the POJO, we pass the class as generic.

    interface IObservable<CLASS> {
        val manager: ObservableManager<CLASS>
    }
    
    open class Observable<CLASS> : IObservable<CLASS> {
        override val manager = ObservableManager<CLASS>()
    }
    
  4. To add the functionality to register properties and listeners, we use extension-functions, such that we don’t have to repeat us twice (once in the interface and once in the IObservable).

  5. To safely register listeners, we use two generics: the one of the classType an the one for the propertyType. with those two properties. This function needs to be inlined, as we are going to use PROP for casting.

    @Suppress("UNCHECKED_CAST")
    inline fun <CLASS, reified PROP> IObservable<CLASS>.registerListener(
         property: KProperty1<CLASS, PROP>, 
         crossinline consumer: BiConsumer<PROP>
    ) = manager.registerListener(property, consumer)
    
  6. Inside our ObservableManager, we add the functionality for the listeners:

    • We add an inline function to convert a listener witch accepts two params of the propertyType to a listener witch accepts two params of type Any? and cast those params to the propertyType.

      inline fun <reified PROP> registerListener(
          property: KProperty<PROP>, 
          crossinline consumer: BiConsumer<PROP>
      ) = registerListener(property.name) { old, new -> consumer(old as PROP, new as PROP) }
      
    • Then we add the list of the propertyName to the converted consumers (we can safely use the propName, because we checked at registring if the properties being added are valid).

    • We only can make those listeners private if we add another not-inlined function to register the converted listeners.

      private val listeners = mutableMapOf<String, MutableList<BiConsumer<Any?>>>()
      fun registerListener(name: String, biConsumer: BiConsumer<Any?>) {
         listeners.getOrPut(name) { mutableListOf() }.add(biConsumer)
      }
      
  7. The registration of the properties is less straigtforward.
    We have four steps we need to make:
    a. We need to create the extension-function which creates an object we can delegate to. We do this inline, so that we can create a lambda that casts a value of type Any? to PROP. We need this later.

    @Suppress("UNCHECKED_CAST")
    inline fun <reified S, T> IObservable<S>.observable(value: T) = 
            DelegateProvider(manager, value) { a: Any? -> a as T }
    

b. We have the genericType of the value and the initialValue. In order to get the name of the value, we need to return a class with provideDelegate. This function accepts two functions, the class and the property, which we pass on to an innerclass WrappingRWProperty, which we create in the next step. (innerclasses are called upon their receiver, so that we know of which type the innerclass is.

class ObservableValue<CLASS, PROP>(
    private val manager: ObservableManager<S>,
    val value: Any?,
    private val converter: (Any?) -> PROP
) : DelegateProvider<CLASS, PROP> {
    override operator fun provideDelegate(
        thisRef: CLASS,
        prop: KProperty<*>
    ): ReadWriteProperty<CLASS, PROP> = manager.WrappingRWProperty(prop, value, converter)
}

c. Now that we have everything we need, we provide the class with the real delegate. This is the class that has the getValue and setValue functions. These functions are the one being called by the field. This means that the class needs the PROP-generic for getting and setting the value.
We make this class an innerclass of ObservableManager, such that it can acces private methods/fields in ObservableManager. therefor we can store the properties inside the ObservableManager and noone can touch it.

inner class WrappingRWProperty<R>(
    prop: KProperty<*>,
    value: Any?,
    private val converter: (Any?) -> R
) : ReadWriteProperty<T, R> {
    //We can finally register the initial value ;-)
    init { properties[property.name] = value }

    override operator fun getValue(thisRef: T, property: KProperty<*>) = 
        properties[property.name].let(converter)
    override operator fun setValue(thisRef: T, property: KProperty<*>, value: R) =
        updateValue(property.name, value)
}

and of course, the ObservableManager also has the propertiesfielf and the updateValue, where it notifies the listener for updating.

class ObservableManager<T> {
    private val properties = mutableMapOf<String, Any?>()

    private fun updateValue(name: String, value: Any?) {
        val old = properties.put(name, value)
        listeners[name]?.forEach { it(old, value) }
    }
    ...
}

note, doesn’t work well with inheritance, that is you shoudn’t delegate fields from a parent and his child to ObservableManager, as the values will be overwritten

code
typealias BiConsumer<O> = (O, O) -> Unit

class ObservableManager<T> {
    private val properties = mutableMapOf<String, Any?>()
    private val listeners = mutableMapOf<String, MutableList<BiConsumer<Any?>>>()

    private fun updateValue(name: String, value: Any?) {
        val old = properties.put(name, value)
        listeners[name]?.forEach { it(old, value) }
    }

    fun registerListener(name: String, biConsumer: BiConsumer<Any?>) {
        listeners.getOrPut(name) { mutableListOf() }.add(biConsumer)
    }

    inline fun <reified PROP> registerListener(property: KProperty1<T, PROP>, crossinline consumer: BiConsumer<PROP>) =
            registerListener(property.name) { old, new -> consumer(old as PROP, new as PROP) }

    inner class WrappingRWProperty<PROP>(
            prop: KProperty<*>,
            value: Any?,
            private val converter: (Any?) -> PROP
    ) : ReadWriteProperty<T, PROP> {
        //We can register the initial value ;-)
        init {
            properties[prop.name] = value
        }

        override operator fun getValue(thisRef: T, property: KProperty<*>) = properties[property.name].let(converter)
        override operator fun setValue(thisRef: T, property: KProperty<*>, value: PROP) = updateValue(property.name, value)
    }
}

class ObservableValue<CLASS, PROP>(
        private val delegater: ObservableManager<CLASS>,
        val value: Any?,
        private val converter: (Any?) -> PROP
) : DelegateProvider<CLASS, PROP> {
    override operator fun provideDelegate(
            thisRef: CLASS,
            prop: KProperty<*>
    ): ReadWriteProperty<CLASS, PROP> = delegater.WrappingRWProperty(prop, value, converter)
}

interface DelegateProvider<CLASS, PROP> {
    operator fun provideDelegate(
            thisRef: CLASS,
            prop: KProperty<*>
    ): ReadWriteProperty<CLASS, PROP>
}


interface IObservable<CLASS> {
    val manager: ObservableManager<CLASS>
}

open class Observable<CLASS> : IObservable<CLASS> {
    override val manager = ObservableManager<CLASS>()
}


@Suppress("UNCHECKED_CAST")
inline fun <reified CLASS, PROP> IObservable<CLASS>.observable(value: PROP): DelegateProvider<CLASS, PROP> = ObservableValue(manager, value) { a: Any? -> a as PROP }

inline fun <CLASS, reified PROP> IObservable<CLASS>.registerListener(property: KProperty1<CLASS, PROP>, crossinline consumer: BiConsumer<PROP>) = manager.registerListener(property, consumer)


class Test : IObservable<Test> by Observable() {
    var test by observable("old")
}

fun main(args: Array<String>) {
    val test = Test()
    test.registerListener(Test::test) { old, new ->
        print("test: $old -> $new")
    }
    test.test = "new" // prints "test: old -> new"
}

#3

what an awesome way to do !


#4

Thing is there are a few very different ways to implement observers not just in syntax but in functionality, so creating a cookie cutter version for the “language standed” could be a bit confusing/stifling. But it’s easy enough to make your own. Personally I prefer this kind of syntax for Delegates that I need underlying access to:

class Observable<T>(detfault: T) {
	var underlying = default
		set(value) {
			// Trigger on change
		}
		
	fun addObserver( onChange : (old : T, new:T)->Unit)	{/*etc*/}
	fun removeObserver( onChange : (old : T, new:T)->Unit) {/*etc*/}
		
	operator fun getValue(thisRef: Any, prop: KProperty<*>): T = underlying
	operator fun setValue(thisRef:Any, prop: KProperty<*>, value: T) {underlying = value}
}

class Thing {
    val textObserver: Observable<String>
	val text by textObserver
}

fun main() {
	val thing = Thing("rex")
	thing.textObserver.addObserver{ old, new -> 
		println("From $old to $new")
	}
	
	thing.text = "spot"
}

Sure you duplicate the variables on things that need access to them, but it’s clean.


#5

Yep, this looks a lot like the way tornadofx does it. They use javafx properties and they need to pass it on. This is the only way you can get a reference to the JavaFX-property of an field.

I like it only for that reason, because it makes a dataclass literally twice as long.


#6

Or in case you already use Reactive Java you can leverage it and all of its transformation capabilities instead of creating your own observer class. To bridge reactive observables to Kotlin delegation I have this short class.

class BehaviorSubjectProperty<T>(
    private val subject: BehaviorSubject<T>
) : ReadWriteProperty<Any?, T> {
    override fun getValue(thisRef: Any?, property: KProperty<*>): T = subject.value
    override fun setValue(thisRef: Any?, property: KProperty<*>, value: T) = subject.onNext(value)

}

Then to use it you can simply declare a backing property and delegate the public ones to it:

class ViewModel {
    private val _firstNames = BehaviorSubject.createDefault("")
    public val firstNames: Observable<String> get() = _firstNames.skip(1)
    public var firstName: String by BehaviorSubjectProperty(_firstNames)
}

Now you have the current firstName and updating it will be propagated as well as a stream of changes via the plural firstNames val and you can do anything the reactive API gives you.