17

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?

Azim Salimov
  • 305
  • 1
  • 3
  • 13
  • What is stopping you from collecting in one scope itself? Like if you launch `lifecycleScope.launchWhenStarted {}` can't you just `stocksVM.quotes.collect{}` and `stocksVM.stockUpdate.collect {}` inside it only. – che10 Jun 02 '21 at 06:21
  • 4
    Unfortunately, I can't. Because collect() is a suspending function inside a coroutine and it will suspend until my flow stops, thus my next collect() will not be called until the previous flow terminates @che10 – Azim Salimov Jun 03 '21 at 07:30

4 Answers4

45

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

Róbert Nagy
  • 6,720
  • 26
  • 45
  • I tried it exactky this way and it doesn't work. Only the first launch{} works. Subsequent launch blocks lie there with no action. – Akash Gorai Mar 18 '22 at 12:00
9

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
Future Deep Gone
  • 831
  • 6
  • 16
8

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)
    }
}
Rawlin Crasto
  • 195
  • 2
  • 8
7

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)
Tenfour04
  • 83,111
  • 11
  • 94
  • 154
  • This requires `implementation 'androidx.lifecycle:lifecycle-runtime-ktx:2.4.0'` dependency. More here -> https://medium.com/androiddevelopers/a-safer-way-to-collect-flows-from-android-uis-23080b1f8bda – lincollincol Mar 30 '22 at 09:25