0

By the docs, I should never collect the flow from launch or launchIn, but if I try to change my code to be executed from inside the repeatOnLifecycle it is just being ignored.

This is the code I'm using right now in my Fragment:

  private fun observeUI() {
        viewLifecycleOwner.lifecycleScope.launch {
            launch {
                viewModel.uiState.flowWithLifecycle(viewLifecycleOwner.lifecycle)
                    .collect { uiState ->
                        uiState.error?.let { showErrorAlert(it)}
                        uiState.success?.let { handleSuccessState(it) }
                        handleLoadingState(uiState.isLoading)
                    }
            }
            repeatOnLifecycle(Lifecycle.State.STARTED) {
                scanner.barcode.collect { handleScannedBarcode(it) }
            }
        }
    }

The function is called from onViewCreated and works as have to.

But if I try to refactor the function by moving the UI collect in repeatOnLifecycle that will stop working.

What I'm trying to do is this, and I've yet tried to use CREATED instead of STARTED:

private fun observeUI() {
    viewLifecycleOwner.lifecycleScope.launch {
        repeatOnLifecycle(Lifecycle.State.STARTED) {
            viewModel.uiState.flowWithLifecycle(viewLifecycleOwner.lifecycle)
                .collect { uiState ->
                    uiState.error?.let { showErrorAlert(it)}
                    uiState.success?.let { handleSuccessState(it) }
                    handleLoadingState(uiState.isLoading)
                }
            scanner.barcode.collect { handleScannedBarcode(it) }
        }
    }
}
NiceToMytyuk
  • 3,644
  • 3
  • 39
  • 100

1 Answers1

2

It's because you moved viewModel.uiState...collect out of its own child launch block, so it's no longer running in parallel with the scanner.barcode.collect code. They're running sequentially, but collect doesn't return until a Flow is complete. Likely your Flow is infinite. It definitely is if it's a SharedFlow or StateFlow.

These need to be in separate coroutines. You can wrap all but the last of your collect calls in launch { }, or you can wrap them all for symmetry.

private fun observeUI() {
    viewLifecycleOwner.lifecycleScope.launch {
        repeatOnLifecycle(Lifecycle.State.STARTED) {
            launch {
                viewModel.uiState
                    .collect { uiState ->
                        uiState.error?.let { showErrorAlert(it)}
                        uiState.success?.let { handleSuccessState(it) }
                        handleLoadingState(uiState.isLoading)
                }
            }
            launch { 
                scanner.barcode.collect { handleScannedBarcode(it) } 
            }
        }
    }
}

Edit: the format I like to use to avoid deeply indented code:

private fun observeUI() {
    viewModel.uiState
        .onEach { uiState ->
            uiState.error?.let { showErrorAlert(it)}
            uiState.success?.let { handleSuccessState(it) }
            handleLoadingState(uiState.isLoading)
        }
        .flowWithLifecycle(viewLifecycleOwner.lifecycle, Lifecycle.State.STARTED)
        .launchIn(viewLifecycleOwner.lifecycleScope)

    scanner.barcode
        .onEach { handleScannedBarcode(it) }
        .flowWithLifecycle(viewLifecycleOwner.lifecycle, Lifecycle.State.STARTED)
        .launchIn(viewLifecycleOwner.lifecycleScope)
}

I actually use an extension function to combine the last two operators since there's some redundancy there.

fun <T> Flow<T>.launchWithLifecycleOwner(lifecycleOwner: LifecycleOwner, state: Lifecycle.State): Job =
    flowWithLifecycle(lifecycleOwner.lifecycle, state).launchIn(lifecycleOwner.lifecycleScope)
Tenfour04
  • 83,111
  • 11
  • 94
  • 154
  • Awesome explanation, I've managed to keep it in separate coroutines instead of wrapping it out :) – NiceToMytyuk Apr 27 '23 at 07:28
  • 1
    I also missed that you had a `flowWithLifecycle` in there. That's redundant to using `repeatOnLifecycle`. Actually, if you use `flowWithLifecycle`, it's perfectly fine to use `launchIn`. – Tenfour04 Apr 27 '23 at 07:33
  • So is it equivalent using flowWithLifecycle or it's better anyway to use a separate coroutine like [here](https://i.gyazo.com/b66ff3d4c4f23be096bd3d89f6389a92.png) – NiceToMytyuk Apr 27 '23 at 08:02
  • 1
    Any way you do it, you're creating at least two coroutines. They might be children of a shared coroutine, or they could both be base level coroutines. I prefer `flowWithLifecycle` followed immediately by `launchIn` and then I never call `launch`. I just have independent coroutines (no shared parent). Then there's a lot less code indention going on. – Tenfour04 Apr 27 '23 at 08:17
  • Is it possible to have an example of how would you manage it with independent coroutines and `launchIn`? – NiceToMytyuk Apr 27 '23 at 08:21
  • 1
    I just updated the answer. – Tenfour04 Apr 27 '23 at 08:22
  • Let us [continue this discussion in chat](https://chat.stackoverflow.com/rooms/253344/discussion-between-nicetomytyuk-and-tenfour04). – NiceToMytyuk Apr 27 '23 at 08:47