Revisiting unidirectional data flows on Android with Kotlin’s SharedFlow and StateFlow

Leandro Martín Peralta
3 min readApr 9, 2021

--

I was glad to hear that Kotlin now has support for hot flows or, in the words of the official documentation:

(…) [a flow whose] active instance exists independently of the presence of collectors (…)

For those of us coming from the world of Reactive Streams (RxJava in my case), these would be the equivalent to Subjects and Processors.

The reason I am glad we have hot flows, in the form of SharedFlow and StateFlow, is that I can replicate my previous implementation of uni-directional data flow with RxJava using coroutines and flows instead.

Let’s code

To demonstrate my approach I will use the same example I used for the RxJava version of this solution. However, I will introduce some improvements to how the model for this application will be defined.

The Event and State definitions will stay the same:

sealed class CoinTosserEvent {
object Toss : CoinTosserEvent()
object Heads : CoinTosserEvent()
object Tails : CoinTosserEvent()
}
data class CoinTosserState(
val isTossing: Boolean = false,
val isHeads: Boolean = false
)

But instead of using a Reducer class I will simply use a function:

fun reduce(
state: CoinTosserState,
event: CoinTosserEvent
) = when (event) {
CoinTosserEvent.Toss -> state.copy(
isTossing = true
)
CoinTosserEvent.Heads -> state.copy(
isTossing = false,
isHeads = true
)
CoinTosserEvent.Tails -> state.copy(
isTossing = false,
isHeads = false
)
}

And finally, I will introduce a new concept to handle side-effects. Enter the Reactor:

class CoinTosserReactor() {

suspend fun react(
event: CoinTosserEvent
): CoinTosserEvent? = when (event) {
is CoinTosserEvent.Toss -> {
delay(1000)
when {
Math.random() < .5 -> CoinTosserEvent.Heads
else -> CoinTosserEvent.Tails
}
}
else -> null
}
}

I chose the term Reactor for this component to evoke the phrase “every action has a reaction”. The react method takes and Event and may or may not produce an Event in return.

In essence, here we can declare all our side-effects (e.g., network requests, database queries). If an Event produces no side-effects or if it is of no consequence to the logic of our application (e.g., logging), we just return a null value. Why am I not using a pure function for this as well? Because it is very likely I will need other dependencies for my side effects, like repositories or interactors.

StateFlow and SharedFlow

Now let’s get to the core of this solution and re-implement the ViewModel for our example app:

class CoinTosserViewModel : ViewModel() {

private val events = MutableSharedFlow<CoinTosserEvent>()
private val state = MutableStateFlow(CoinTosserState())

val liveState: LiveData<CoinTosserState> = state.asLiveData()

private val reactor = CoinTosserReactor()

init {

viewModelScope.launch {

launch {
events
.mapNotNull(reactor::react)
.collect { launch { events.emit(it) } }
}

launch {
events
.map { reduce(state.value, it) }
.collect(state::emit)
}
}
}

fun onToss() {
viewModelScope.launch { events.emit(CoinTosserEvent.Toss) }
}
}

At the top we have defined our streams of events and states. Next we expose the state to the view as LiveData and instantiate our reactor.

Now, what is happening inside the init block? We are basically doing 2 things:

First, we start collecting the events, mapping the non-null values through our reactor. If our reactor produces and event (i.e.: Heads or Tails in this example), we emit that event back into the event stream. Basically, side-effects are now being handled. Notice we launch a new coroutine to emit an event back into the same flow (not doing so would cause issues).

Second, we start collecting changes to our state by mapping new events through our reduce function and emitting the resulting state back into the state flow.

Finally, we have a public method which emits Toss events into the events flow. And that is it!

Conclusion

I hope this example helps you structure your applications making use of coroutines and flows. I am still refining the approach, so please get comfortable with it before using it in production. I am sure you will find ways to improve on it and adapt it to your own coding style.

You can find the source code for the example here.

--

--

Leandro Martín Peralta
Leandro Martín Peralta

Responses (1)