Coroutine in Android: Working with Lifecycle

Abir Hasan Zoha
AndroidPub
Published in
7 min readAug 9, 2019
Image by Johannes Plenio from Pixabay

In this series of blog posts, I am going to explore Kotlin Coroutine and it’s usage in the Android world.

  1. Coroutine in Android: Basic
  2. Coroutine in Android: Working with Lifecycle (You are here)
  3. Coroutine in Android: Working with Retrofit (credit: zsmb13)
  4. Coroutine in Android: Working with Room Database
  5. Coroutine in Android: Working with Workmanager

In the previous blog post, I have discussed the Basic of Kotlin Coroutine. In this blog post, I am going to discuss the coroutine support in LifeCycle library. I have assumed that you are familiar and have working experience with Lifecycle library.

Lifecycle library has three main artifacts. These artifacts provide us with three main APIs to work with Coroutine.

  1. LifecycleScope in lifecycle-runtime from version 2.2.0-alpha01
  2. ViewModelScope in lifecycle-viewmodel from version 2.1.0-beta01
  3. livedata in lifecycle-livedata from version 2.2.0-alpha01

To have all these APIs available in your project, add the following dependencies in the build.gradle file:

def livedata_version = "2.2.0-alpha02"
implementation "androidx.lifecycle:lifecycle-viewmodel-ktx:$livedata_version"
implementation "androidx.lifecycle:lifecycle-runtime-ktx:$livedata_version"
implementation "androidx.lifecycle:lifecycle-livedata-ktx:$livedata_version"

LifecycleScope

A LifecycleScope is defined for each Lifecycle & LifecycleOwner object. Any coroutine launched in this scope is canceled when the Lifecycle is destroyed.

From Activity you can access LifecycleScope in two ways:

lifecycle.coroutineScope.launch {...}
lifecycleScope.launch {...}

And from Fragment you can access LifecycleScope in three ways:

viewLifecycleOwner.lifecycleScope.launch {...}
lifecycle.coroutineScope.launch {...}
lifecycleScope.launch {...}

Regardless of how you access the LifesycleScope, you will get the same CoroutineScope.

LifecycleScope is bound to Dispatcher.Main. That means if you don’t change Dispatcher explicitly all the coroutines inside LifecycleScope will be executed on the main thread.

lifecycleScope

Handling Exception in LifecycleScope

LifecycleScope uses SupervisorJob. In the SupervisorJob, if any child coroutine failed then this failure is not propagated to any other siblings. But if the parent gets canceled then all the children get canceled.

A failure of a child job that was created using lifecycleScope.launch{…} can be handled via CoroutineExceptionHandler. The lifecycleScope.launch(…) takes an optional CoroutineExceptionHandler parameter.

here exception in the second coroutine won’t stop the first coroutine. It will be handled by the CoroutineExceptionHandler

Here one more thing I want to mention is that if you use nested launch{…} and pass handler on the nested launch{…} it won’t have any effect. Only if you call launch{…} using the lifecycleScope then the passed handler will handle the exception.

only lifecycleScope.launch(…) use handler to handle Exception

A failure of a child job that was created using lifecycleScope.async{…} can be handled via Deferred.await on the resulting deferred value.

handling Exception on Deferred.await using a try-catch block

Suspend Lifecycle-aware coroutines

Alongside with the default coroutine builders, the lifecycleScope provides three special coroutine builders:

lifecycleScope.launchWhenCreated {...}
lifecycleScope.launchWhenStarted {...}
lifecycleScope.launchWhenResumed {...}

Lifecycle also provides three special suspending functions to have the same functionalities: lifecycle.whenCreated, lifecycle.whenStarted, and lifecycle.whenResumed. You can call them from any suspending function or coroutine.

Notice: here log from the coroutine was executed 2s after the onStart method called. Before onStart, the coroutine was suspended.

Any coroutine run inside these blocks is suspended if the Lifecycle isn’t at least in the minimal desired state. The example above contains a code block that runs only when the associated Lifecycle is at least in the STARTED state.

If the Lifecycle moves to a lesser state while the block is running, the block will be suspended until the Lifecycle reaches a state greater or equal to minState.

Here the block from launchWhenStarted get’s suspended when the lifecycle moves to lesser State.

For simplicity you can remember that:

  1. launchWhenCreated {…} gets suspended when onDestry() is called or user clear the app from backStack
  2. launchWhenStarted {…} gets suspended when onStop() is called
  3. launchWhenResumed {…} gets suspended when onPause() is called

Note that this won’t affect any sub coroutine if they use a different CoroutineDispatcher. However, the block will not resume execution when the sub coroutine finishes unless the Lifecycle is at least in minState.

Here, the block from different Dispatcher continues to run even after the onStop method is called. But after returning from different Dispatcher the block from launchWhenStarted get suspended as the State of the lifecycle is less than Started.

ViewModelScope

A ViewModelScope is defined for each ViewModel in your app. Any coroutine launched in this scope is automatically canceled if the ViewModel is cleared.

From ViewModel, you can access the ViewModelScope by the extension property viewModelScope.

using viewModelScope inside any ViewModel

This ViewModelScope is bound to Dispatchers.Main, that means by default all code will be executed in the Main thread. And it uses the SupervisorJob. So the exception handling of ViewModelScope is the same as the LifecycleScope we have just learned before.

Coroutines with LiveData

In ViewModel, we mostly use LiveData to serve data from ViewModel to Activity or Fragment. We can get the LiveData asynchronously by using ViewModelScope. But to make our life easier LiveCycle library provides us liveData builder function. The liveData building block serves as a structured concurrency primitive between coroutines and LiveData

emitting data from liveData builder function

The coroutine created by liveData uses Dispatchers.Main by default. That means by default every code inside liveData block will be executed in the Main thread.

liveData builder function takes two optional param CoroutineContext, and TimeOut in millis. TimeOut is used to wait before cancellation.

Note: Do not pass CoroutineContext here unless you really need to.

example with timeout & context

The block inside liveData follows some special rules:

  1. The block inside liveData doesn’t start executing immediately. It starts executing when the returned LiveData is active.
  2. If the LiveData becomes inactive while the block is executing, it will be canceled after TimeOut unless the LiveData becomes active again before that timeout. Any value emitted from a canceled block will be ignored.
  3. After cancellation, if the LiveData becomes active again, the block will be re-executed from the beginning.
  4. If the block completes successfully or is canceled due to reasons other than LiveData becoming inactive, it will not be re-executed even after LiveData goes through the active inactive cycle.

emit(…)

Inside liveData building block, you can call emit(…) function to set the LiveData’s value. This function offers the main safety. That means you can call this function from any thread and each emit(…) call suspends the execution of the block until the LiveData value is set on the main thread.

calling emit(…) from different Dispatcher

You can also emit multiple values from the block.

calling emit(…) multiple times from a single liveData{…} block

emitSource(…)

Inside liveData building block, you can call emitSource(...) function to set a LiveData as a source for the returned LiveData. This is similar to MediatorLiveData.addSource(…). If the given LiveData get changed then the returned LiveData will also be changed automatically.

Calling emit(…) and emitSource(…) will remove any source that was yielded before via emitSource(…). Also, this function returns DisposableHandle by which the source LiveData can be removed.

Like emit(...), this function also offers main safety. So you can call this function from any thread.

simple emitSource example

You can also call emitSource(..) multiple times from the block.

calling emitSource multiple times

latestValue

latestValue is a property which you can access from liveData building block. It references the current value of the LiveData. If the block never emitted a value, latestValue will be null.

If the liveData building block gets re-executed because of TimeOut and If you would like to continue the operations based on where it was stopped last, you can use the latestValue property to get the latest value emitted by the block before it got canceled.

liveData with Transformations

You can combine liveData with Transformations, as shown in the following example:

using liveData with Transformations

In this blog post, I have documented my understanding of Kotlin Coroutine and Lifecycle Library. If you find it helpful please encourage me by clapping ❤️ & following 😄. And feel free to comment if you have any observation or suggestion.

Follow me on => Twitter :: LinkedIn :: Github

--

--