I have 2 stateFlow's in my viewModel. To collect them in fragment I have to launch coroutines 2 times as below:
lifecycleScope.launchWhenStarted {
stocksVM.quotes.collect {
if (it is Resource.Success) {
it.data?.let { list ->
quoteAdapter.submitData(list)
}
}
}
}
lifecycleScope.launchWhenStarted {
stocksVM.stockUpdate.collect {
log(it.data?.data.toString())
}
}
If I have more stateFlow's, I have to launch coroutines respectively. Is there a better way to handle multiple stateFlow's in my Fragment/Activity or wherever?
StateFlow and SharedFlow are Flow APIs that enable flows to optimally emit state updates and emit values to multiple consumers.
Using StateFlowBy using viewLifecyleOwner. lifecycleScope extension, we make the flow consumption lifecycle-aware, just like LiveData does. On destroy, the coroutine context is cancelled. LiveData only emits when the LifecycleOwner is on active state.
In a ViewModel, you have the same risk of collecting from Flows for no reason. To avoid it, you can use stateIn or shareIn with a WhileSubscribed value. Then it will stop collecting when there is nothing downstream collecting.
In coroutines, a flow is a type that can emit multiple values sequentially, as opposed to suspend functions that return only a single value. For example, you can use a flow to receive live updates from a database. Flows are built on top of coroutines and can provide multiple values.
So if we want, for example, to collect all the changes to the StateFlow's value, we can collect them in the same way we did with Flows. If we need to get the value of the StateFlow directly, in a non-reactive way, as it exposes its value, we can access just by calling its getter.
A StateFlow is always active and in memory, and it becomes eligible for garbage collection only when there are no other references to it from a garbage collection root. When a new consumer starts collecting from the flow, it receives the last state in the stream and any subsequent states.
StateFlow and SharedFlow are Flow APIs that enable flows to optimally emit state updates and emit values to multiple consumers. StateFlow is a state-holder observable flow that emits the current and new state updates to its collectors. The current state value can also be read through its value property.
One of the main advantages of a StateFlow is that it can be exposed in two different modes, with only one instance. If we need to access the value in a reactive way, it can be exposed as a Flow. So if we want, for example, to collect all the changes to the StateFlow's value, we can collect them in the same way we did with Flows.
You will need different coroutines, since collect()
is a suspending function that suspends until your Flow
terminates.
For collecting multiple flows the currently recommended way is:
lifecycleScope.launch {
lifecycle.repeatOnLifecycle(Lifecycle.State.STARTED) {
launch {
stocksVM.quotes.collect { ... }
}
launch {
stocksVM.stockUpdate.collect { ... }
}
}
}
Note that the problem with launchWhenStarted
is that while your newly emitted items will not be processed your producer will still run in the background.
I'd definitely give this a read, as it explains the current best-practices really well: https://medium.com/androiddevelopers/a-safer-way-to-collect-flows-from-android-uis-23080b1f8bda
Like @RóbertNagy said, you shouldn't use launchWhenStarted
. But there is an alternate syntax for doing it the proper way without having to do the nested launch
es:
stocksVM.quotes
.flowOnLifecycle(Lifecycle.State.STARTED)
.onEach {
if (it is Resource.Success) {
it.data?.let { list ->
quoteAdapter.submitData(list)
}
}
}.launchIn(lifecycleScope)
stocksVM.stockUpdate
.flowOnLifecycle(Lifecycle.State.STARTED)
.onEach {
log(it.data?.data.toString())
}.launchIn(lifecycleScope)
You can choose to mix multiple streams.
merge
or combine
in kotlin
. Of course, the usage of these two functions is different.Add:
If Flow is not processed, open multiple Coroutines to collect():
fun main() {
collectFlow()
}
fun emitStringElem(): Flow<String> = flow {
repeat(5) {
delay(10)
emit("elem_$it")
}
}
fun emitIntElem(): Flow<Int> = flow {
repeat(10) {
delay(10)
emit(it)
}
}
Open two coroutine collections result is:
From int Flow: item is: 0
From string Flow: item is: elem_0
From int Flow: item is: 1
From string Flow: item is: elem_1
From int Flow: item is: 2
From string Flow: item is: elem_2
From int Flow: item is: 3
From string Flow: item is: elem_3
From int Flow: item is: 4
From string Flow: item is: elem_4
From int Flow: item is: 5
From int Flow: item is: 6
From int Flow: item is: 7
From int Flow: item is: 8
From int Flow: item is: 9
Merge two flows
fun margeFlow() = runBlocking {
merge(
emitIntElem().map {
it.toString()
}, emitStringElem()
).collect {
println(it)
}
}
result is :
0
elem_0
1
elem_1
2
elem_2
3
elem_3
4
elem_4
5
6
7
8
9
conbine two flows:
fun combineFlow() = runBlocking {
combine(emitIntElem(), emitStringElem()) { int: Int, str: String ->
"$int combine $str"
}.collect {
println(it)
}
}
result is:
0 combine elem_0
1 combine elem_0
1 combine elem_1
2 combine elem_2
3 combine elem_3
4 combine elem_4
5 combine elem_4
6 combine elem_4
7 combine elem_4
8 combine elem_4
9 combine elem_4
If someone is wondering how to emit multiple flows in the same block of viewModelScope.launch
, it is the same as Robert's answer. i.e as follows
viewModelScope.launch {
launch {
exampleFlow1.emit(data)
}
launch {
exampleFlow2.emit(data)
}
}
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With