0

Activity code which is collecting the flow as state and updating the UI based on the state emitted by the Flow.

setContent {
    val viewModel: MainViewModel = viewModel()
    val state: State<UiState> = viewModel.flow.collectAsStateWithLifecycle(
            initialValue = UiState.Loading,
            lifecycle,
            minActiveState = Lifecycle.State.STARTED
    )
    MainScreen(uiState = state.value)
}

ViewModel code which is exposing the StateFlow

val flow: Flow<UiState> = repository.flow
        .map {
            if (it.datetime == "") {
                emit(UiState.Error("Failure"))
            } else {
                UiState.Success(it.datetime) 
            } 
        }
        .stateIn(
            scope = viewModelScope,
            started = SharingStarted.WhileSubscribed(5000),
            initialValue = UiState.Loading
        )

Cold flow exposed by the repository:

val flow = flow {
        while (true) {
            delay(5000)
            emit(getCurrentTime())
        }
    }.catch {
        // Emit Empty Object which will be treated as an error 
        emit(CurrentTime(""))
    }

If there is an exception in the upstream cold flow which is exposed by the Repository, there will no more emissions from the upstream flow as it gets completed. We can emit an UiState.Error in such case to the UI (shown above).

Is there a way we can retrigger the upstream flow again once the UI is in error state.

Sumit Trehan
  • 3,985
  • 3
  • 27
  • 42

1 Answers1

1

Catch is a terminal flow operator, therefore the flow is completed afterward.

You can use retry() operator to restart the flow if an exception happens.

val flow = flow {
    while (true) {
        delay(5000)
        emit(getCurrentTime())
    }
}.retry {
    // Emit Empty Object which will be treated as an error 
    emit(CurrentTime(""))
}

If you want to have a way to restart the producer flow from the UI, you need to have some kind of trigger flow, which does not terminate.

Here is an example implementation using a Channel:

private val triggerChannel = Channel<Unit>(Channel.CONFLATED)

private val producerFlow = flow {
    while (true) {
        delay(1000)
        emit("producing")
    }
}.catch {
    emit("uh oh, error!")
}

val uiStateFlow = triggerChannel
    .receiveAsFlow()
    .onStart { triggerChannel.send(Unit) }
    .flatMapLatest {
        producerFlow.map {
            if (it == "producing") {
                UiState.Success
            } else {
                UiState.Error
            }
        }
    }.stateIn(
            scope = viewModelScope,
            started = SharingStarted.WhileSubscribed(5000),
            initialValue = UiState.Loading
    )

fun retry() {
    viewModelScope.launch {
        triggerChannel.send(Unit)
    }
}

This code restarts the producer, when Unit is sent to the channel. Keep in mind, that you need to send the trigger to the channel when you subscribe to the uiStateFlow, that's why onStart { triggerChannel.send(Unit) } is needed.

  • There are 2 flows involved here, 1 is hot which is exposed by the viewModel and the other is cold flow which is exposed by the repository. The stateIn operator converts the cold to hot flow. With retry operator we can have some number of retries. But even if all retries failed, the cold flow will get complete which is the upstream flow for the hot flow. My query is is there a way to retrigger the hot flow again from the UI after the upstream cold flow is complete. – Sumit Trehan Mar 16 '23 at 14:04
  • Understood, my bad, I have updated my answer with a way how to achieve your wanted behavior. Although, I would still recommend `retry`, it has an unlimited retries as an option anyways. – Jokubas Trinkunas Mar 16 '23 at 17:38
  • Why we need to emit onStart to the channel ? – Sumit Trehan Mar 26 '23 at 03:42
  • I understood the concept now, basically the flatMapLatest will cancel any ongoing flow and return a new flow based on the logic in the transform block inside flatMapLatest. To avoid a sending to channel in onStart, we can use a StateFlow instead of Channel and toggle its value in onRetry() method, since StateFlow always holds value and upon collection will emit the initial value and thus we can avoid emit/send in onStart block. – Sumit Trehan Mar 26 '23 at 05:59