Android unidirectional architecture with StateFlow and SharedFlow

David Santos
Eureka Engineering
Published in
6 min readDec 6, 2020

Introduction

There are many ways to name this architecture, Unidirectional data flow, MVI, Redux but all these names are making reference to the same or similar concept: Create a single source of truth and a unidirectional information flow.
By the way, MVI (Model View Intent) might confuse some people because Intent is already used in Android with another meaning. In my opinion “Unidirectional architecture” is better naming.

An example of how it could be (image from https://proandroiddev.com/android-unidirectional-state-flow-without-rx-596f2f7637bb)

Purpose of this article

The main purpose of this article is to make a simple implementation of this architecture with the recent StateFlow from Kotlin, test edge cases and try to solve possible problems.
There are many articles about this architecture on the Internet but I think it would be interesting to try edge use cases, see how we could approach and solve possible problems.

Scope

  • Unidirectional flow architecture
  • Use StateFlow instead of LiveData for binding ViewModel with View

Out of scope

  • Network request
  • Error handling
  • Dependency Injection
  • Clean architecture
  • Multi module project
  • Navigation Jetpack
  • Integrate with Compose Jetpack

Architecture implementation decisions

Some implementations use a Store as a singleton for the whole application state but I prefer to have a concrete Store for one View so I will use ViewModel as Store and bind it to a single screen, so every screen will have its own Store.

Each screen updates on a Store’s change (image from https://android.jlelse.eu/how-flux-saved-my-life-4cb59a5e112a)
Each screen updates on its own Store’s change

There are cases where we would like to use a Store like Notification Store among different screens or features. In that case my approach would be to create a manager object as singleton instead of Store and register the specific Store to this manager so it can receive the notifications and bypass it to the view.

Each screen updates on its own Store’s change with Notification Manager

Many implementations use a reducer function to generate a new state from the current one + action/intent but in this article I wanted to keep it as sample as possible so I will not implement it as a reducer function.

The communication between View and ViewModel (Store) is done by using a SharedFlow for Actions and StateFlow for view State.

So we have a SharedFlow and StateFlow on the ViewModel:

val actionFlow = MutableSharedFlow<GreetingAction>()
private val mutableState = MutableStateFlow(GreetingState(false, "", ""))
val state: StateFlow<GreetingState>
get() = mutableState

And handling the actions sent by the View

init {
viewModelScope.launch {
handleActions()
}
}

On the View side we will emit a new Action trough the SharedFlow whenever is necessary, for instance on click button event.

viewModel.actionFlow.emit(GreetingAction.Greet(nickname?.text.toString()))

And collect view State from ViewModel using the StateFlow.

override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
...
lifecycleScope.launch {
viewModel.state.collect { state -> handleState(state) }
}
}

Points to be careful with StateFlow

  • Initial value is required.
  • With LiveData.observe() automatically unregisters the consumer when the view goes to the STOPPED state, whereas collecting from a StateFlow or any other flow does not. We will see what happens because of this later on this article.
  • Values in StateFlow are conflated so it will not dispatch the same state in a row (similar to distinct on RxJava), so depending on your use case consider using SharedFlow instead.

Application specification

The application basically has two screens (Fragments), one for greeting and another empty just to test edge cases.

The greeting screen simulates an API request using a delay function from Kotlin coroutines. User’s nickname input is sent, after a while a message from a remote source is received.

Edge cases

Let’s start the interesting part!

Interruptions

  • Going to background while loading and wait

View state (StateFlow) is being consumed even when background state.
This behavior could waste resources when is not necessary besides is not recommendable to access to view elements while the app is on background since it could provoke crash errors.
Possible solution:

lifecycleScope.launchWhenStarted {
viewModel.state.collect { state -> handleState(state) }
}

With launchWhenStarted will create a coroutine for collecting StateFlow when the view is at least in the Started state and will stop the coroutine (pause collecting) when the view moves to the Stopped state. LiveData behaves like this out of the box.

But after trying this approach, it created a new problem. As many times as the view enters to the Started state, coroutines are being created. So If we navigate to another view and come back to previous view, a new coroutine will be created thus view state will be collected twice and so on.
Solution to this problem:

override fun onStart() {
super.onStart()
uiStateJob = lifecycleScope.launch {
viewModel.state.collect { state -> handleState(state) }
}
}

override fun onStop() {
uiStateJob?.cancel()
super.onStop()
}

Basically it manually stops collecting the StateFlow and replace the previous Job with a new one when entering to Started state.

  • Rotation while loading

Since we are using StateFlow, it will collect the last UI state value, and recreate the view with it. No problems here!

  • Navigation to next view (Fragment) while loading

Since ViewModel is not cleared until Fragment is totally destroyed, when remote call is completed, result will stored into StateFlow, then going back to the first screen will collect last UI state and show the message.

Process death

Some time ago SavedStateHandle was released with the purpose of easily keep data even after process death. It is integrated with ViewModel and works quite well with LiveData using the method getLiveData. So integrating it to StateFlow, becomes a little bit unnatural but you can work around getting the saved state when ViewModel is created and set the new values in parallel when emitting them using StateFlow.

init {
...

savedStateHandle.get<GreetingState>("state")?.let { savedState ->
mutableState.value = savedState
}
}
...savedStateHandle["state"] = mutableState.value.copy(
isLoading = true,
successMessage = "",
failureMessage = ""
)

Conclusion

Looks like StateFlow can be used instead of LiveData, but we still have to perform some workarounds to get the same behavior as LiveData.

The good part is that StateFlow has more operators than LiveData, close to RxJava operators. Also it is native Kotlin so we are decoupling from Android SDK and maybe getting closer to KMM (Kotlin multiplatform mobile) in the case we use Store object independent from ViewModel.

I can not strongly recommend either one or other, I think it depends on which is your app architecture. If you are using ViewModel as Store or binding component for your Views, then probably LiveData fits better. But If you are using a pure Kotlin native Store approach then StateFlow could fit better. Also don’t forget you can transform a StateFlow to LiveData using asLiveData() method.

Repository

https://github.com/DavidEure/unidirectional-arch/

Reference links

--

--