Coroutines Flow vs Suspend Function, Sequence and livedata

nddquang
3 min readSep 26, 2020

--

In this blog, I will make a brief compare between Flows with other way to return data when we deal with coroutines such as: suspend fun, sequence and livedata.

When using suspend function in Coroutines, most of the time it allows us return only one time:

suspend fun demoSuspendFun(): List<Int> {
delay(1000)
return listOf(1, 2, 3, 4, 5, 6)
}

This way is okay if we want to all data return at once. What if we have three functions, each of function return 2 items after delay 1 second:

suspend fun longTask1(): List<Int> {
delay(1000)
return listOf(1, 2)
}

suspend fun longTask2(): List<Int> {
delay(1000)
return listOf(3, 4)
}

suspend fun longTask3(): List<Int> {
delay(1000)
return listOf(5, 6)
}

With suspend function, we have to implement like this:

suspend fun demoSuspendFun(): List<Int> {
val result = mutableListOf<Int>()
result.addAll(longTask1())
result.addAll(longTask2())
result.addAll(longTask3())
return result
}

With this implement, we have to wait for about 3 seconds before processing all 6 items. We can check this by running this:

fun main() = runBlocking {
val startTime = System.currentTimeMillis()

demoSuspendFun().forEach {
println("receive $it after ${System.currentTimeMillis() - startTime}ms")
}
}

Result:

I wonder if we have other ways to handle every 2 items when they become available?

Just escape from coroutine a little bit. In Kotlin, if our tasks are synchronous tasks, we can use Sequence. This way we can process each task:

fun longSyncTask1(): List<Int> {
Thread.sleep(1000)
return listOf(1, 2)
}

fun longSyncTask2(): List<Int> {
Thread.sleep(1000)
return listOf(3, 4)
}

fun longSyncTask3(): List<Int> {
Thread.sleep(1000)
return listOf(5, 6)
}
fun demoSequence() = sequence {
yield(longSyncTask1())
yield(longSyncTask2())
yield(longSyncTask3())
}

Let’s check if it works as we expected. First we write a main function for this case:

demoSequence().forEach {
println("receive $it after ${System.currentTimeMillis() - startTime}ms")
}

This time, we receive every 2 items after 1 second:

Now if we try to call suspend fun inside Sequence, it raises warning because Sequence not support that. We have to use Flow instead:

fun demoFlow() = flow {
emit(longTask1())
emit(longTask2())
emit(longTask3())
}

Then we check result by this code:

demoFlow().collect {
println("receive $it after ${System.currentTimeMillis() - startTime}ms")
}

We get the same result as Sequence. The different is that in this case, we handle with suspend functions:

Flow with Livedata

Sometimes, we want our function return to livedata to make it easy for observering from UI layer, we do something like this:

fun getIntList() = liveData { 
emit(listOf(1, 2, 3))
}

It’s okay but if we already have flow, we can use the extension “.asLiveData()” to make the same. It helps us to reduce poilerplate code.

To explain why it helps us to reduce the complexity. Livedata has “.switchMap” to transform from livedata to another livedata, like this:

fun fetchWeather(): LiveData<Int> {
...
}

val currentWeatherLiveData: LiveData<String> =
fetchWeatherFlow()
.switchMap {
liveData {
emit(heavyTransformation(it))
}
}

Using switchMap, it requires many code level. It will make it hard to debug . Using Flow, we can make the above become more simple like this:

fun fetchWeatherFlow() = flow<Int> {

}

val currentWeatherLiveData: LiveData<String> =
fetchWeatherFlow()
.map { heavyTransformation(it) }
.asLiveData()

That’s why in Repository or Datasource, it’s better to use Flow instead of LiveData.

Preferences

--

--