A common pattern used to observe the state of a feature can look something like this (borrowed from NowInAndroid):
ViewModel.kt
val uiState: StateFlow<MainActivityUiState> = userDataRepository.userId.map {
MainActivityUiState.Loaded(it)
}.stateIn(
scope = viewModelScope,
initialValue = MainActivityUiState.Loading,
started = SharingStarted.WhileSubscribed(5_000),
)
This works well for fetching some data, emitting a starting value, and emitting that data when the fetch is completed. Let's consider a scenario where that data fetching failed and we would like for our user to be able to retry the operation that powers our state.
The Problem
For us to allow the user to be able to retry the operation(s) that power the state of the feature, we must first implement a mechanism that allows us to 'retry' the flow. This mechanism will be able to trigger a retry a flow to re-emit a value to a state provider.
The Solution
class RetryableFlowTrigger {
internal val retryEvent: MutableStateFlow<RetryEvent> = MutableStateFlow(RetryEvent.INITIAL)
fun retry() {
retryEvent.value = RetryEvent.RETRYING
}
}
fun <T> RetryableFlowTrigger.retryableFlow(
flowProvider: RetryableFlowTrigger.() -> Flow<T>,
): Flow<T> {
return retryEvent
.onSubscription {
// reset to initial state on each new subscription so that the original flow can be re-evaluated
retryEvent.value = RetryEvent.INITIAL
}
.filter {
// allow retry and initial events to trigger the flow provider
it == RetryEvent.RETRYING || it == RetryEvent.INITIAL
}
.flatMapLatest {
// invoke the original flow provider
flowProvider.invoke(this)
}
.onEach {
// reset to idle on each value
retryEvent.value = RetryEvent.IDLE
}
}
internal enum class RetryEvent {
RETRYING,
INITIAL,
IDLE,
}
The usage of our new class looks like this:
ViewModel.kt
val retryableFlowTrigger = RetryableFlowTrigger()
val uiState : StateFlow<MainActivityUiState> = retryableFlowTrigger.retryableFlow {
userDataRepository.userId.map.map {
MainActivityUiState.Loaded(it)
}
}.stateIn(
scope = viewModelScope,
initialValue = MainActivityUiState.Loading,
started = SharingStarted.WhileSubscribed(5_000),
)
fun retryFlow() {
retryableFlowTrigger.retry()
}
Now, whenever you would like to provide UI to retry the flow, invoking retryFlow
in your `ViewModel` will retry the flow.