Exploring Different Ways to Collect Kotlin Flow

vtsen

Vincent Tsen

Posted on January 13, 2023

Exploring Different Ways to Collect Kotlin Flow

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

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) 
        } 
    } 
}
Enter fullscreen mode Exit fullscreen mode

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)
}
Enter fullscreen mode Exit fullscreen mode

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>)
Enter fullscreen mode Exit fullscreen mode

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)  
}
Enter fullscreen mode Exit fullscreen mode

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 }
Enter fullscreen mode Exit fullscreen mode

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  
            } 
        }
    }
}
Enter fullscreen mode Exit fullscreen mode

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
            }
        }   
    }
}
Enter fullscreen mode Exit fullscreen mode

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
                }
            }
        }
    }
}
Enter fullscreen mode Exit fullscreen mode

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)
    /*...*/
}
Enter fullscreen mode Exit fullscreen mode

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 Implementations ON_STOP / Background (not visible) ON_START / Background (visible) ON_DESTROY (Configuration Changed) ON_DESTROY (Lifecycle Death)
collectAsState() Keeps emitting Keeps emitting Cancels emitting Cancels emitting
viewModelScope.launch() Keeps emitting Keeps emitting Keeps emitting Cancels emitting
launchWhenStarted() Suspends emitting Resumes emitting Cancels emitting Cancels emitting
repeatOnLifecycle(Lifecycle.State.STARTED) Cancel emitting Restarts Emitting Cancels emitting Cancels 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)


Originally published at https://vtsen.hashnode.dev.

💖 💪 🙅 🚩
vtsen
Vincent Tsen

Posted on January 13, 2023

Join Our Newsletter. No Spam, Only the good stuff.

Sign up to receive the latest update from our blog.

Related