Skip to content
This repository has been archived by the owner on Aug 12, 2022. It is now read-only.

Signals

nbilyk edited this page Dec 21, 2020 · 11 revisions

Signals are one of the ways Acorn UI accomplishes an Observer Pattern.

Signals in Acorn UI are based off the Signals and Slots paradigm from Qt. A Signal has a list of callback handlers, and those handlers are invoked with the type-checked event provided in the signal's dispatch(event).

A simple example of a Signal representing a change event on an object.

class Sample(owner: Context) : ContextImpl(owner) {

    val changed = signal<ChangeEvent<Int>>()

    var value by observable(0) {
        prop, old, new ->
        changed.dispatch(ChangeEvent(old, new))
    }
}

fun main() {
    val s = Sample()
    s.changed.listen { e ->
        println("Changed from: ${e.oldData} to ${e.newData}")
    }
    s.value = 3 // prints "Changed from: 0 to 3"
}

// It is also possible to remove handlers by the disposable handle returned by `listen`.

fun main2() {
    val s = Sample()
    val changedSub = s.changed.listen(::changedCallback)
    s.changed.once(::changedCallback2)
    s.value = 3 // prints "Changed from: 0 to 3" then prints "Another changed handler" and removes the `changedCallback2` handler.
    changedSub.dispose()
    s.value = 4 // no print
}

private fun changedCallback(event: ChangeEvent) {
    println("Changed from: ${event.oldData} to ${event.newData}")
}

private fun changedCallback2(event: ChangeEvent) {
    println("Another changed handler")
}

Concurrency Rules

  • A Signal may not be dispatched while it's currently dispatching.
  • A Signal may remove a handler from a handler (including itself).
  • A Signal may add another handler from a handler.
  • If a handler is added within a handler, the new handler will NOT be invoked in the current dispatch.
  • If a handler is removed within a handler, the removed handler will not be invoked in the current dispatch (unless it already was).

Tips

  • Signals are typically named in past tense after the verb they represent. With the exception of a Signal representing an opportunity to cancel an action.

Example:

class Sample(owner: Context) : ContextImpl(owner) {

    val changing = signal<DataChangeEvent<Int>>())

    val changed = signal<DataChangeEvent<Int>>())

    var value: Int = 0
        set(value) {
            if (field == value) return
            val e = ChangeEvent(field, value)
            changing.dispatch(e)
            if (!e.defaultPrevented) {
                field = value
                changed.dispatch(e)
            }
        }
}

The changing and changed events are separated to avoid complexity with priority and ordering. For example an observer that wants to perform an update after value has changed should not need to ensure that its handler priority is lower than other handlers that may invoke preventDefault().

NB: It is a good practice to dispose a Signal when the containing class is disposed. This happens automatically if using the signal dsl within a class that implements Owner. (In this example ContextImpl)

Clone this wiki locally