Kotlin Flow: Handling state using SharedFlow and StateFlow

268 views Asked by At

I have this use case where response from data source are guaranteed to be consume and not missed after configuration change, but also do not repeat it when already consume.

StateFlow seems to be the best candidate as it holds last emitted value but can be problematic on a following scenario:

  1. Data source response with the same error on each request (maybe due to having no data in local cache or network issue.)

As StateFlow does not emit the same value, this can be problematic where after a new request to data source, an error state (can be dialog) will not show again in the UI if the previous state is also error.

Using SharedFlow with configured replay

MutableSharedFlow<T>(replay = 1, onBufferOverflow = BufferOverflow.DROP_OLDEST)

This will behave like a LiveData with repeatOnLifecycle, but has its own issue on the following scenario.

Let say we have these states

sealed class AssetState {
    data class FetchAssetLoading(val assetList: List<AssetMinDataDomain>?) : AssetState()
    data class FetchAssetSuccess(val assetList: List<AssetMinDataDomain>) : AssetState()
    data class FetchAssetFailed(val msg: String, val assetList: List<AssetMinDataDomain>?) : AssetState()
}

Inside onCreate or onViewCreated

// Initial fetch
fetchData(param1, param2)

// Collecting the result of fetch data 
with(viewModel) {
   viewLifecycleOwner.collectShared(assetState, ::onAssetStateChanged)
}

Extension function

inline fun <T : Any, L : SharedFlow<T>> LifecycleOwner.collectShared(
    sharedFlow: L,
    crossinline function: (T) -> Unit,
    lifecycleState: Lifecycle.State = Lifecycle.State.STARTED
) {
    lifecycleScope.launch {
        repeatOnLifecycle(lifecycleState) {
            sharedFlow.collect { t -> function(t) }
        }
    }
}

When a configuration change happens, the fetchData(param1, param2) will be triggered and its result will be collected when the LifeCycle at least reached STARTED but since we also use repeatOnLifecycle there will be two emission of data. One as a result of request fetchData which is more updated and one as the result of repeatOnLifecycle which can be outdated. The only solution I see as of the moment is to add a empty state or no state then manually set it after receiving success state of failed state in the UI but seems not the best solution and too repetitive. Is there a better way to satisfy the above requirements? Thanks

P.S

If I remove repeat = 1 in SharedFlow it will solve the issue, however if the activity went onStop e.g. the user press home button while fetching data then went back to the app causing onStart and onResume to trigger then we will lose that event and its result.

1

There are 1 answers

0
Bitwise DEVS On BEST ANSWER

As of now, to perform single event using flows is using Channel and turn it to regular cold flow like this

ViewModel

private val _assetState = Channel<AssetState>()

val assetState = _assetState.receiveAsFlow()

To emit use send

// Some method 
_assetState.send(AssetState.FetchLoading())

Collecting the result of fetch data

with(viewModel) {
   viewLifecycleOwner.collectFlow(assetState, ::onAssetStateChanged)
}

Update extension function

inline fun <T : Any, L : Flow<T>> LifecycleOwner.collectFlow(
    flow: L,
    crossinline function: (T) -> Unit,
    lifecycleState: Lifecycle.State = Lifecycle.State.STARTED
) {
    lifecycleScope.launch {
        repeatOnLifecycle(lifecycleState) {
            flow.collect { t -> function(t) }
        }
    }
}

Note that there is a subtle catch on using this approach where you can still missed an event if Channel sent the event but the consumer just in time suddenly went background based on the set LifeCycle.

Reference: https://medium.com/androiddevelopers/viewmodel-one-off-event-antipatterns-16a1da869b95