Coroutine in Android: Working with Lifecycle
In this series of blog posts, I am going to explore Kotlin Coroutine and it’s usage in the Android world.
- Coroutine in Android: Basic
- Coroutine in Android: Working with Lifecycle (You are here)
- Coroutine in Android: Working with Retrofit (credit: zsmb13)
- Coroutine in Android: Working with Room Database
- 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.
- LifecycleScope in lifecycle-runtime from version 2.2.0-alpha01
- ViewModelScope in lifecycle-viewmodel from version 2.1.0-beta01
- 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.
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 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.
A failure of a child job that was created using lifecycleScope.async{…} can be handled via Deferred.await on the resulting deferred value.
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.
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.
For simplicity you can remember that:
- launchWhenCreated {…} gets suspended when onDestry() is called or user clear the app from backStack
- launchWhenStarted {…} gets suspended when onStop() is called
- 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.
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.
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
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.
The block inside liveData follows some special rules:
- The block inside liveData doesn’t start executing immediately. It starts executing when the returned LiveData is active.
- 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.
- After cancellation, if the LiveData becomes active again, the block will be re-executed from the beginning.
- 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.
You can also emit multiple values from the 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.
You can also call emitSource(..) multiple times from the block.
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: