Exploring Different Ways to Collect Kotlin Flow
Vincent Tsen
Posted on January 13, 2023
Simple app to demonstrate Kotlin flow(), emit(), collectAsState(), collect(), viewModelScope.launch(), launchWhenStarted() and repeatOnLifecycle()
This is part of the asynchronous flow series:
Part 3 - Exploring Different Ways to Collect Kotlin Flow
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()
withLifeCycle.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 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.
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.
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
January 26, 2024