Flow’s asLiveData() with CoroutineContext parameter

Another question from my colleague: we use Flow inside ViewModel and convert to LiveData which Fragment observes. When we should set the context parameter when using asLiveData()?

Bram Yeh
3 min readNov 20, 2021

Our team has had lots of trivial but funny discussions. This is another question that we didn’t understand well at the beginning of using Flow, we don’t know when we should assign this parameter and what we need to set. I would like to share our investigation and conclusion, and … if it’s wrong, please feel free to inform me.

Generally, we use Flow’s asLiveData() before Fragment observes, but this extension function has two parameters: context and timeoutInMs.

@JvmOverloads 
fun <T> Flow<T>.asLiveData(
context: CoroutineContext = EmptyCoroutineContext,
timeoutInMs: Long = DEFAULT_TIMEOUT
): LiveData<T>

The timeoutInMS has the default value, DEFAULT_TIMEOUT, which is 5000ms, because of a workaround for configuration changes and other situations. And about context: CoroutineContext, most of the time we just neglect and pass by the default value, EmptyCoroutineContext. EmptyCoroutineContext has no elements in it, semantically it is an empty coroutine context, doesn’t change any behavior, then Dispatchers.Default is used.

asLiveData() will receive the CoroutineContext to collect the upstream Flow. If there is no context parameter, it uses the default EmptyCoroutineContext combined with Dispatchers.Main.immediate by wrap SupervisorJob.

  • asLiveData() creates CoroutineLiveData
public fun <T> Flow<T>.asLiveData(
context: CoroutineContext = EmptyCoroutineContext,
timeoutInMs: Long = DEFAULT_TIMEOUT
): LiveData<T> = liveData(context, timeoutInMs) {
collect {
emit(it)
}
}
public fun <T> liveData(
context: CoroutineContext = EmptyCoroutineContext,
timeoutInMs: Long = DEFAULT_TIMEOUT,
@BuilderInference block: suspend LiveDataScope<T>.() -> Unit
): LiveData<T> = CoroutineLiveData(context, timeoutInMs, block)
  • CoroutineLiveData combined received context with Dispatchers.Main.immediate
class CoroutineLiveData<T>(
context: CoroutineContext = EmptyCoroutineContext,
timeoutInMs: Long = DEFAULT_TIMEOUT,
block: Block<T>
) : MediatorLiveData<T>() {

init {
val supervisorJob = SupervisorJob(context[Job])
val scope = CoroutineScope(Dispatchers.Main.immediate + context + supervisorJob)
}
}

When we directly use asLiveData(), it will handle the flow in the main thread by default.

There is an example of why LiveData needs the different CoroutineContext in the Use coroutines with LiveData. In this case, it’s saying that you want a coroutine that’s bound to the lifecycle of your ViewModel, and it executes on the IO thread as opposed to the main thread.

class MyViewModel: ViewModel() {
private val userId: LiveData<String> = MutableLiveData()
val user = userId.switchMap { id ->
liveData(context = viewModelScope.coroutineContext + Dispatchers.IO) {
emit(database.loadUserById(id))
}
}
}

If it only gives liveData(viewModelScope.coroutineContext), all the suspend function database.loadUserById() that will execute with Dispatchers.Main because CoroutineLiveData uses the Dispatchers.Main.immediate.

It’s common to use flowOn to switch threads, for example

class MyViewModel: ViewModel() {
private val userId: Flow<String> = .....
val user = userId
.map { database.loadUserById(id) } // Will be executed in IO
.flowOn(Dispatchers.IO)
.asLiveData(context = viewModelScope.coroutineContext)
}

Not only to switch threads, but sometimes to be consistent in lifecycles between Flow and ViewModel, we will take the viewModelScope.coroutineContext and set it into asLiveData(viewModelScope.coroutineContext), to take advantage of built-in cancellation for Flow, the upstream flow will have automatically cancellation when ViewModel is destroyed.

This example comes from Android MVI architecture with Jetpack & Coroutines/Flow — Part 2, written by Pavlos-Petros Tournaris

Conclusion

If the flow is generated by the ViewModel itself, when Fragment or Activity destroys, the ViewModel will also be destroyed, then the flow will also be canceled and cleaned. A viewModelScope is defined for each ViewModel, and any coroutine launched in this scope is automatically canceled when the ViewModel is cleared. In this case, it’s totally fine to use asLiveData() without any context parameter.

But, if the flow comes from an external source, such as some DB or repository, to automatically cancel the flow when ViewModel becomes destroyed, it’s better (or we prefer) to use asLiveData(viewModelScope.coroutineContext).

However, we need to know, if one Fragment has only one ViewModel, when the Activity/Fragment moves to the STOPPED state, the coroutine will suspend, the Flow producer will suspend along and nothing else will happen until the coroutine is resumed. That means, even we use asLiveData() without viewModelScope.coroutineContext, we won’t face critical issues usually.

Afterword

A Flow itself is not lifecycle-aware, the responsibility of syncing with the lifecycle is moved up to the coroutine collecting the Flow.

In general, a Fragment has just one ViewModel, when the Fragment is destroyed, the ViewModel will also destroy. However, sometimes the ViewModel is shared between multi fragments, the Fragment’s lifecycle may not match this ViewModel’s lifecycle. To make LiveData’s lifecycle align with ViewModel, we generally convert Flow to LiveData inside ViewModel.

--

--