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.
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.
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.
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
}
}
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.
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.