Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Collect from several stateflows

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?

like image 813
Azim Salimov Avatar asked Jun 02 '21 06:06

Azim Salimov


People also ask

What is StateFlow and SharedFlow?

StateFlow and SharedFlow are Flow APIs that enable flows to optimally emit state updates and emit values to multiple consumers.

Is StateFlow lifecycle aware?

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.

How does ViewModel collect flow?

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.

What is flow in Kotlin?

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.

How to get the value of a Stateflow?

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.

How does garbage collection work in a Stateflow?

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.

What is the difference between Stateflow and sharedflow?

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.

What are the advantages of Stateflow over flows?

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.


4 Answers

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 image 139
Róbert Nagy Avatar answered Oct 16 '22 16:10

Róbert Nagy


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 launches:

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)
like image 22
Tenfour04 Avatar answered Oct 16 '22 15:10

Tenfour04


You can choose to mix multiple streams.

Use the function 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
like image 25
Future Deep Gone Avatar answered Oct 16 '22 15:10

Future Deep Gone


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)
    }
}
like image 5
Rawlin Crasto Avatar answered Oct 16 '22 16:10

Rawlin Crasto