Contents

Usage of SharedFlow

onCreate

With the 1.4 Update, kotlin coroutines brought us a lot of features and improvements. One of them was the introduction of SharedFlow. Basically, what I would like to call it, is simply a “Single Event StateFlow”. Remember single event LiveData workaround? Well, here you have it, but 100% kotlin, and not a workaround anymore. If you are more interested in an even more detailed explanation, follow this issue on Github, brought by Roman Elizarov.

onResume

The problem

Let’s suppose we have this scenario:

After a simple calculation on the ViewModel, we want to decide in which screen the user is going to be navigated to. Let’s do that by using StateFlow (MutableStateFlow):

private val _state = MutableStateFlow<State>(State.Idle)
val state: StateFlow<State> get() = _state

fun checkCalculations(){
    if(calculationsSuccessful())
       _state.value = State.MoveForward
    else
      _state.value = State.ShowError
}

Until here, nothing new or problematic. Now, let’s check what is happening on our fragment:

viewModel.state
    .onEach{ state ->
        when(state){
            MoveForward -> navigateToNextScreen(addToBackstack = true)
            else -> showError()
        }
    }.launchIn(lifecycleScope)

Seems also fine here. But it’s not!

If the user wants to go back, they will for sure, but guess what happens: They get re-routet to the screen they were already, moving the navigation back and forward, everytime they press the back button.

onPause

There is always a posibility for hacks:

viewModel.state
    .onEach{ state ->
        when(state){
            MoveForward -> {
                navigateToNextScreen(addToBackstack = true)
                viewModel.backtoIdleState()
            }
            else -> showError()
        }
    }.launchIn(lifecycleScope)

Then, in the ViewModel:

fun backtoIdleState(){
    _state.value = Idle
}

But not really a fan of these solutions.

onResume

Let’s see how the problem is solved by using SharedFlow (and how simple it is). Having the same scenario, but instead of:

private val _state = MutableStateFlow<State>(State.Idle)
val state: StateFlow<State> get() = _state

fun checkCalculations(){
    if(calculationsSuccessful())
       _state.value = State.MoveForward
    else
      _state.value = State.ShowError
}

we use:

private val _events = MutableSharedFlow<Event>()
val events = _events.asSharedFlow()

fun checkCalculations(){ 
    viewModelScope.launch(Dispatchers.Default){ //or whichever Dispatcher you need
        if(calculationsSuccessful())
           _events.emit(event)
      else
         _events.emit(event)
    }
}

In order to emit() from the SharedFlow, we need to use a coroutine or a suspend function. But that is all that needs to be changed (and in this case, the naming).

But what changes on the observers side? Nothing, absolutely nothing. We don’t have to go back to idle manually, and we don’t have to change a single line of it, and the bug is fixed. If the user hits back, they will be routed normally to the previous Fragment easily:

viewModel.state
    .onEach{ event ->
        when(event){
            MoveForward -> navigateToNextScreen(addToBackstack = true)
            else -> showError()
        }
    }.launchIn(lifecycleScope)

That’s because SharedFlow is an implementation of Flow already.

onDestroy

SharedFlow looks nice and promising, as many of the latest coroutines APIs. You can also use SharedFlow as a pure EventBus. Even though, this would make your debugging a little harder. For more information and special operations about SharedFlow, please visit he official docs here.