Skip to content

Instantly share code, notes, and snippets.

@elizarov
Last active December 26, 2019 15:18
Show Gist options
  • Save elizarov/4840d4f7c953bd2dfd4f256065e554c1 to your computer and use it in GitHub Desktop.
Save elizarov/4840d4f7c953bd2dfd4f256065e554c1 to your computer and use it in GitHub Desktop.
StateFlow Design Draft

StateFlow Design Draft

UI applications often need to maintain observable state. The state is conceptually a time-changing variable that always has a value that can be observed by multiple views. Currently, ConflatedBroadcastChannel is used for such state representation but it is not very efficient, because it has to provide full-blown implementation of SendChannel for updating state and ReceiveChannel for observing state, but far simpler concept is actually needed in practice.

Proposal

Introduce a StateFlow interface that is a Flow with an additional readable value property:

interface StateFlow<out T> : Flow<T> {
    val value: T
}

A MutableStateFlow interface allows mutatation of the value:

interface MutableStateFlow<T> : StateFlow<T> {
    override var value: T
}

A construction function is called StateFlow(...) even though it returns MutableStateFlow (similarly to Job() returning CompletableJob()):

fun <T> StateFlow(initial: T): MutableStateFlow<T>

Initial value is always required. Conceptually, state flow always has some initial value. For every collector of the StateFlow it immediately emits the current value followed by all updates to the state flow's value until the collector is cancelled.

Conflation

State flow is always conflated. It means that in back-to-back updates to the value only the most recent one is delivered to collectors. Applying conflate() operator to the state flow does not do anything. However, not every conflated flow is a state flow, hence the resulting type of conflate() operator is Flow, but not StateFlow, because state flow has an additional property of always mantatining the current value that is available immediately without the need to wait for it.

Usage

A typical use of StateFlow is to define a private variable of type MutableStateFlow and to expose it via getter as StateFlow, which give both access to it current value of some Type and gives ability to react to the changes in the state via Flow<Type>:

class SomeModel {
    private val _someState = StateFlow<Type>(initialValue)
    val someState: StateFlow<Type> get() = _someState
     
    fun updateState() {
        // a simple assignment to value makes it emit new value to collectors
        _someState.value = newValue 
    }
}

Reactive vs imperative

The most controversial decision in StateFlow design is the fact that it exposes its state both via reactive interface of Flow<T> and via the imperative val value: T. Conceptually, it is possible to expose the state only via the reactive type of Flow<T>. You can always query the current of any flow via first() terminal operation, but this operator is a suspending function, which is ineffcient for cases where presence of current state value is always guaranteed.

In additional to that, the inability to simply query the current state of a reactive stream often leads to prolifiration of operators like withLatestFrom which just queries the lates value of one of the reactive stream without reacting on its change. For example, if an application needs to react to a stream of some "click event" by taking snapshot of the current state to somehow process it (dispay, save, etc), then in a fully reactive ways it will be written as:

eventFlow.withLatestFrom(someState) { event, value -> 
    ... // process event that happened at state value. 
}

However, with ability to simply access the current state value one can rewrite it without having to provide a dedicated operator which aligns quite nicely with the flow's goal to minimize the number of provided operators:

eventFlow.map { event, value -> 
    ... // use someState.value as needed
}

The downside of the latter approach is that now there is no automatic snapshot of the current state value so two accesses to someState.value may return different value, which might be counter-intititive and can lead to subtle bugs.

Update non-atomicity and glitches

The above discussion on reactive vs imperative leads to discussion on non-atomicity of updates. Consider this code, which is maintains a state of two integers and its update function maintains invariant that their sum is always zero:

class TwoCounters {
    private val _a = StateFlow<Int>(0)
    private val _b = StateFlow<Int>(0)
    
    val a: StateFlow<Int> get() = _a
    val b: StateFlow<Int> get() = _b
     
    fun update() {
        _a.value++
        _b.value--
    }
}

However, observer reading state's value either via .value or reacting to changes via Flow might see violation of this invaint, e.g. the following test will fail:

combine(twoCounters.a, twoCounters.b) { a, b -> 
    assertEquals(0, a + b) // will fail
}

This is a also known as a glitch. It also shows in more complicated case with additional data-processing pipelines and can lead to counter-intuitive behavior where application reacts in a destructive ways (crashes, saves wrong data to db, etc) by oberving inconsistent state.

This problem can be solved by employing some form of software transaction management, but these kinds of solution an quite expensive in terms of runtime and library-size overhead. More importantly, for cases like the one shown here it still requires developer to rember to explicitly demarcate the bound of their atomic data changes, that is to rember about the presence of this problem, so the proposed decision is not to do anything about it.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment