Home > Software engineering >  StateFlow setting value drops some events, but updating doesn't
StateFlow setting value drops some events, but updating doesn't

Time:05-03

In my app, I have a UIState sealed class to represent the UI state.

sealed class UIState<T> {
    class ShowLoading<T> : UIState<T>()
    class HideLoading<T> : UIState<T>()
    class ShowEmptyData<T> : UIState<T>()
    class ShowData<T>(val data: T) : UIState<T>()
    class ShowError<T>(val errorUIState: ErrorUIState) : UIState<T>()
}

Accordingly, my viewmodel code is:

 someRequest.apply { response ->
     when (response) {
         is ApiResponse.Success -> {
             _uiStateFlow.value = (UIState.HideLoading()) // First, hide the loading
             // Do some work
             _uiStateFlow.value = (UIState.ShowData(data))
         }
         is ApiResponse.Error -> {
             _uiStateFlow.value = (UIState.ShowError(error))
         }
     }
 }

In this case, most of the time my hideLoading state doesn't collect, it drops because the success/error state comes immediately after hideLoading, and my UI doesn't collect it. E.g. If I will delay success/error states set for 100ms, hideLoading will be collected from the UI. I use collect instead of collectLatest.

But then I've found out, that when I change the set value part to update {}, the UI collects all the states.

someRequest.apply { response ->
    when (response) {
        is ApiResponse.Success -> {
            _uiStateFlow.update { (UIState.HideLoading()) } // First, hide the loading
            // Do some work
            _uiStateFlow.update { (UIState.ShowData(data)) }
        }
        is ApiResponse.Error -> {
            _uiStateFlow.update { (UIState.ShowError(error)) }
        }
   }
}

So what's the difference between .value and update and why this one works pretty well? Thank you.

P.S. I've used emit() as well. Under the hood it's the same as .value, it's just a suspend function.

CodePudding user response:

As stated in the StateFlow documentation:

Updates to the value are always conflated.

Conflated means if values are posted faster than they are collected, then the collector only gets the most recent result. This allows values to always be posted to the StateFlow without having to wait for old values to be collected.

As for why update is allowing that loading state through, I suspect it is only because an atomic update generally takes a little bit longer, and so the collector usually wins the race, in this specific case on your specific test device. This is not a reliable solution to ensuring collectors get all intermediate values.

I'm not seeing why you need a HideLoading state in the first place. Your UI can simply hide the loading state automatically when it receives data to show. Logically, the loading is complete when the data is returned.

If you truly do need this HideLoading state, you should use a SharedFlow with a replay value that is large enough to ensure it is not skipped. But this presents other problems, like collectors possibly getting outdated data to show because it's being replayed to them.

Side note, your sealed class should probably be a sealed interface since it holds no state, and its children that hold no state can be objects with a generic type of Nothing so you don't have to keep instantiating them just to use them and never have to needlessly specify a generic type when they never hold that type anyway. And since the other ones are data wrappers, they might as well be data classes. Like this:

sealed interface UIState<out T> {
    object ShowLoading : UIState<Nothing>
    // object HideLoading : UIState<Nothing>
    object ShowEmptyData : UIState<Nothing>
    data class ShowData<out T>(val data: T) : UIState<T>
    data class ShowError(val errorUIState: ErrorUIState) : UIState<Nothing>
}
  • Related