Retryable StateFlow

Retryable StateFlow

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.