To create reactive applications we need proper mechanisms, idea for Signal is borrowed from FRP (Functional Reactive Programming). With couple additions to better support Android app lifecycle, signals become very powerful, yet simple, building blocks for our apps. They are especially useful in UI development.

What is Signal

In general, Signal is just a value that changes over time, and provides change notifications, a bit like Observable. Very simplified signal interface provides methods to set and observe current value, it could look something like this:

trait Signal[T] {
  def set(value: T): Unit
  def subscribe(f: T => Unit): Unit
}

Cell in a spreadsheet

Signals are much more then just simple observable, they are composable. In this regard, signals can be compared to cells in a spreadsheet, where value of one cell can be defined as a function taking inputs from other cells. Changing a value of one signal may affect changes of all dependant signals, and this change is propagated automatically. Creating an application in practice means defining a huge graph of interconnected signals, possibly with cycles. Thanks to couple special properties, changes in this graph can be propagated very efficiently.

Signals and Reactive Streams

Signals may seem very similar to Reactive Extensions, signal can be viewed as a stream of change events, and event stream could be converted into signal using scan operator. But this comparison can be misleading, with signals, we don't really care about the events, we are only interested in current state. Actually, we are mostly interested in final state, after all changes are applied. This difference has significant implications in how signals can be used compared to event streams.

Signal properties

Empty

Signals can be empty. This is especially useful when data is loaded asynchronously.

Non-contiguous

Signal can also become empty at any point of time. Subscribers are not notified when signal becomes empty. This should be treated as intermediate state, eventually every signal should have a value. If missing signal value is normal state then Signal[Option[...]] should be used.

Skipping states

Signal is allowed to skip intermediate states, as long as final value is eventually propagated. This means that even if signal value is being changed multiple times, we may only get one notification, or even no notification if final state is the same as the one previously generated.

It may seem that signals are unreliable, but in practice we don't really wan't to know about this intermediate changes, they would just generate flickering in the UI (if signal is bound to some view). This property improves performance, and we are still guaranteed to be notified of the final value, so our UI will eventually be updated.

Lazy

Signals are lazy, changes are only propagated if there is some active subscriber waiting for them. Signals also stop propagating changes if recomputed value is equal to previous one. Thanks to this, only part of our dependency graph has to be recomputed on each changes.

Eventually Consistent

During change propagation, individual signals in a graph may have inconsistent values, changes are propagated asynchronously and may produce different state depending recalculation order. But in the end, when all signals are processed, whole signals graph is guaranteed to be consistent.

Thread-safe

Signals API is thread-safe, signals can be accessed and modified from any thread. By default changes are propagated on the same thread that changed the value, but there are ways to force specific execution context on each step. Developer should keep in mind that when accessing thread sensitive data during signal processing.

Composing Signals

Signals implement typical monadic operators: map and flatMap, those work similar to respective operations in RxJava.

val fieldSignal = signal map (_.field)
val composedSignal = signal flatMap service.signalFromValue

Sample usage

signal ! Value(...)
signal mutate (_ + 10) // atomic
signal.currentValue() : Option[T]
signal.on(Threading.Ui) { value => ... }

Signal lifecycle

  • corresponds to lifecycle context
    • global
    • activity
    • fragment
    • view
  • every subscribe is done in some context (usually implicit)
  • subscribers will not be notified when context is not active
  • automatic unsubscribe when context is destroyed

Auto-wiring

EventStream


blog comments powered by Disqus