Exploring Different Ways to Collect Kotlin Flow

Simple app to demonstrate Kotlin flow(), emit(), collectAsState(), collect(), viewModelScope.launch(), launchWhenStarted() and repeatOnLifecycle()

ยท

5 min read

Exploring Different Ways to Collect Kotlin Flow

This is part of the asynchronous flow series:

Basic Kotlin Flow Usages

You first need to create a flow before you can collect it.

Create Flow<T> and Emit T Value

1. Create Flow<T> Using kotlinx.coroutines.flow.flow()

The first parameter of the flow() is the function literal with receiver. The receiver is the implementation of FlowCollector<T> interface, which you can call FlowCollector<T>.emit() to emit the T value.

2. Emit T Value Using FlowCollector<T>.emit()

In this code example, the T is an Int. This flow emits Int value from 0 โ†’ 10,000 with a 1-second interval delay.

class FlowViewModel: ViewModel() 
{ 
    val flow: Flow<Int> = flow { 
        repeat(10000) { value -> 
            delay(1000) 
            emit(value) 
        } 
    } 
}

The delay(1000) simulates the process of getting the value that you want to emit (from a network call for example)

Once we have the flow, you can now collect the flow. There are different ways of collecting flow:

  • Flow.collectAsState()

  • Flow.collect()

Collect Flow - Flow.collectAsState()

collectAsState() uses the poducestate() compose side effect to collect the flow and automatically convert the collected value to State<T> for you.

@Composable
fun FlowScreen() {
   val viewModel: FlowViewModel = viewModel()

   val flowCollectAsState =
       viewModel.flow.collectAsState(initial = null)
}

Collect Flow - Flow.collect()

To collect flow, you need to call Flow<T>.collect() with FlowCollector<T> implementation. The function definition looks like this:

public suspend fun collect(collector: FlowCollector<T>)

The first parameter of the Flow<T>.collect() is the FlowCollector<T> interface, which only has one emit() function.

public fun interface FlowCollector<in T> {    
    public suspend fun emit(value: T)  
}

It means you need to implement the FlowCollector<T> interface, pass in the instance of the implementation as the first parameter to the Flow<T>.collect() function.

However, if you look at the collect() usage below, it doesn't look like the pass-in parameter is the implementation of FlowCollector<T> interface?

flow.collect { value -> _state.value = value }

This is a short-cut way of implementing FlowCollector<T> interface using SAM conversion. Because FlowCollector<T> is functional interfacing, using SAM conversion is allowed. The pass-in lambda is the override function of the FlowCollector<T>.emit() function interface.

When Flow<T>.emit() is called in the flow that we created earlier, this line _state.value = value is executed. value is the parameter that you pass into the emit() function during the flow production in FlowViewModel above.

Since Flow.collect() is a suspend function, you need to launch a coroutine to call it. These are three ways you can launch a coroutine:

  • ViewModel.viewModelScope.launch()

  • LifeCycleCoroutineScope.launchWhenStarted()

  • LifeCycleCoroutineScope.launch() with LifeCycle.repeatOnLifeCycle()

1. Collect Flow Using ViewModel.viewModelScope.launch{}

class FlowViewModel: ViewModel() {  
    private val _state: MutableState<Int?> = mutableStateOf(null)  
        val state: State<Int?> = _state  

    fun viewModelScopeCollectFlow() {   
        viewModelScope.launch {  
            flow.collect {  value ->
                _state.value = value  
            } 
        }
    }
}

2. Collect Flow Using LifeCycleCoroutineScope.LaunchWhenStarted()

class FlowViewModel: ViewModel() {
    private val _state: MutableState<Int?> = mutableStateOf(null)
    val state: State<Int?> = _state

    fun launchWhenStartedCollectFlow(lifeCycleScope: LifecycleCoroutineScope) {
        lifeCycleScope.launchWhenStarted {
            flow.collect { value ->
                _state.value = value
            }
        }   
    }
}

3. Collect Flow Using LifeCycle.RepeatOnLifecycle()

class FlowViewModel: ViewModel() {
    private val _state: MutableState<Int?> = mutableStateOf(null)
    val state: State<Int?> = _state

    fun repeatOnCycleStartedCollectFlow(
        lifeCycleScope: LifecycleCoroutineScope,
        lifeCycle: Lifecycle) {

        lifeCycleScope.launch {
            lifeCycle.repeatOnLifecycle(Lifecycle.State.STARTED) {
                flow.collect { value ->
                    _state.value = value
                }
            }
        }
    }
}

Similarly, you can also call Flow.collectAsStateWithLifecycle() composable function to achieve the same result as LifeCycle.RepeatOnLifecycle(). The code is much shorter and you must call it within a composable function.

@Composable
fun FlowScreen() {
    /*...*/
    val flowCollectAsStateWithLifeCycle = 
        viewModel.flow.collectAsStateWithLifecycle(initialValue = null)
    /*...*/
}

Investigate Various Collect Flow Implementations

Let's assume you have already started collecting flow, this is the summary of what happens in different lifecycle events.

Collect Flow ImplementationsON_STOP / Background (not visible)ON_START / Background (visible)ON_DESTROY (Configuration Changed)ON_DESTROY (Lifecycle Death)
collectAsState()Keeps emittingKeeps emittingCancels emittingCancels emitting
viewModelScope.launch()Keeps emittingKeeps emittingKeeps emittingCancels emitting
launchWhenStarted()Suspends emittingResumes emittingCancels emittingCancels emitting
repeatOnLifecycle(Lifecycle.State.STARTED)Cancel emittingRestarts EmittingCancels emittingCancels emitting

So if you care about saving resources(i.e. not emitting anything when the app is in background (not visible), you can either use launchWenStarted() or repeatOnLifecycle(Lifecycle.State.STARTED). If you want to suspend and resume flow emission, you use launchWenStarted(). If you want to restart the flow emission all over again, you use repeatOnLifecycle(Lifecycle.State.STARTED)

On the other hand, if you don't care about wasting resources, you can use either collectAsState() or viewModelScope.launch(). If you want the flow to keep emitting even after the configuration changed (e.g. screen rotation), you use viewModelScope.launch().

I think it is better to show the flow chart instead.

Exploring_Asynchronous_Kotlin_Flow_Usages_and_Behaviors_Flow_Chart.drawio.png

What I don't understand is this article here saying the launchWhenStarted() is not safe to collect because it keeps emitting in the background when the UI is not visible.

However, I don't see this behavior based on the experiment that I have done. It suspends the flow emission and resumes it when the UI is visible again.

To see more details on launchWhenStarted() and repeatOnLifeCycle(), refer to this article:

Conclusion

I have been using viewModelScope.launch() to collect flow. If the flow doesn't constantly emit value, allowing the flow to emit in the background doesn't waste any resources, in my opinion. Also, I don't need to care about canceling, restarting or resuming the flow emission.

On the other hand, if the flow is constantly emitting values, you may want to consider using either launchWhenStarted() or repeatOnLifecycle(Lifecycle.State.STARTED).

Source Code

GitHub Repository: Demo_AsyncFlow (see the FlowActivity)

Did you find this article valuable?

Support Vincent Tsen by becoming a sponsor. Any amount is appreciated!

ย