Android Coroutine Recipes

Table of Contents
- How to launch a coroutine
- How to launch a coroutine with a timeout
- How to cancel a coroutine
- How to handle exceptions with coroutines
- How to test coroutines
- How to log coroutine thread
Source code for this article is available at dmytrodanylyk/coroutine-recipes
Based on coroutine version v1.0.0
Dependencies
implementation
'org.jetbrains.kotlinx:kotlinx-coroutines-core:1.0.0'implementation
'org.jetbrains.kotlinx:kotlinx-coroutines-android:1.0.0'
How to launch a coroutine
In the kotlinx.coroutines
library, you can start new coroutine using either launch
or async
function.
Conceptually, async
is just like launch
. It starts a separate coroutine which is a light-weight thread that works concurrently with all the other coroutines. The difference is that launch
returns a Job
and does not carry any resulting value, while async
returns a Deferred
- a light-weight non-blocking future that represents a promise to provide a result later. You can use .await()
on a deferred value to get its eventual result, but Deferred
is also a Job
, so you can cancel it if needed.
If the code inside the launch
terminates with exception, then it is treated like uncaught exception in a thread crashes Android applications. An uncaught exception inside the async
code is stored inside the resulting Deferred
and is not delivered anywhere else, it will get silently dropped unless processed.
Coroutine dispatcher
In Android we usually use two dispatchers:
uiDispatcher
to dispatch execution onto the Android main UI thread (for the parent coroutine).bgDispatcher
to dispatch execution in the background thread (for the child coroutines).
// dispatches execution into Android main thread
val uiDispatcher: CoroutineDispatcher = Dispatchers.Main// represent a pool of shared threads as coroutine dispatcher
val bgDispatcher: CoroutineDispatcher = Dispatchers.I0
In following example we are going to use CommonPool
for bgContext
which limit the number of threads running in parallel to the value 64 threads or the number of cores (whichever is larger).
You may want to consider using newFixedThreadPoolContext
or your own implementation of the cached thread pool.
Coroutine scope
To launch coroutine you need to provide CoroutineScope
or use GlobalScope
.
See also: Avoid usage of global scope.
// GlobalScope example
class MainFragment : Fragment() {
fun loadData() = GlobalScope.launch { ... }
}// CoroutineScope example
class MainFragment : Fragment() {
val uiScope = CoroutineScope(Dispatchers.Main)
fun loadData() = uiScope.launch { ... }
}// Fragment implements CoroutineScope example
class MainFragment : Fragment(), CoroutineScope {
override val coroutineContext: CoroutineContext
get() = Dispatchers.Main
fun loadData() = launch { ... }
}
launch + async (execute task)
The parent coroutine is launched via the launch
function with the Main
dispatcher.
The child coroutine is launched via the async
function with the IO
dispatcher.
Note: A parent coroutine always waits for the completion of all its children.
Note: if an unchecked exception occurs, the application will crash.
See also: Avoid usage of unnecessary async/await
val uiScope = CoroutineScope(Dispatchers.Main)fun loadData() = uiScope.launch {
view.showLoading() // ui thread val task = async(bgDispatcher) { // background thread
// your blocking call
}
val result = task.await() view.showData(result) // ui thread
}
launch + withContext (execute task)
While the previous example works fine, we waste resources by launching the second coroutine to do background job. We can optimize our code if we only launch one coroutine and use withContext
to switch coroutine context.
The parent coroutine is launched via the launch
function with the Main
dispatcher.
Background job is executed via withContext
function with the IO
dispatcher.
val uiScope = CoroutineScope(Dispatchers.Main)fun loadData() = uiScope.launch {
view.showLoading() // ui thread val result = withContext(bgDispatcher) { // background thread
// your blocking call
} view.showData(result) // ui thread
}
launch + withContext (execute two tasks sequentially)
The parent coroutine is launched via the launch
function with the Main
dispatcher.
Background job is executed via withContext
function with the IO
dispatcher.
Note: result1
and result2
are executed sequentially.
Note: if an unchecked exception occurs, the application will crash.
val uiScope = CoroutineScope(Dispatchers.Main)fun loadData() = uiScope.launch {
view.showLoading() // ui thread
val result1 = withContext(bgDispatcher) { // background thread
// your blocking call
}
val result2 = withContext(bgDispatcher) { // background thread
// your blocking call
}
val result = result1 + result2
view.showData(result) // ui thread
}
launch + async + async (execute two tasks parallel)
The parent coroutine is launched via the launch
function with the Main
dispatcher.
The child coroutines are launched via the async
function with the IO
dispatcher.
Note: task1
and task2
are executed in parallel.
Note: if an unchecked exception occurs, the application will crash.
See also: Wrap async calls with coroutineScope or use SupervisorJob to handle exceptions.
val uiScope = CoroutineScope(Dispatchers.Main)fun loadData() = uiScope.launch {
view.showLoading() // ui thread
val task1 = async(bgDispatcher) { // background thread
// your blocking call
}
val task2 = async(bgDispatcher) { // background thread
// your blocking call
}
val result = task1.await() + task2.await()
view.showData(result) // ui thread
}
How to launch a coroutine with a timeout
If you want to set a timeout for a coroutine job, wrap the suspended function with the withTimeoutOrNull
function which will return null in case of timeout.
val uiScope = CoroutineScope(Dispatchers.Main)fun loadData() = uiScope.launch {
view.showLoading() // ui thread val task = async(bgDispatcher) { // background thread
// your blocking call
} // suspend until task is finished or return null in 2 sec
val result = withTimeoutOrNull(2000) { task.await() } view.showData(result) // ui thread
}
How to cancel a coroutine
job
The function loadData
returns a Job
object which may be cancelled. When the parent coroutine is cancelled, all its children are recursively cancelled, too.
If the stopPresenting
function was called while dataProvider.loadData
was still in progress, the function view.showData
will never be called.
val uiScope = CoroutineScope(Dispatchers.Main)
var job: Job? = nullfun startPresenting() {
job = loadData()
}fun stopPresenting() {
job?.cancel()
}
fun loadData() = uiScope.launch {
view.showLoading() // ui thread val result = withContext(bgDispatcher) { // background thread
// your blocking call
} view.showData(result) // ui thread
}
parent job
The other way to cancel coroutine is to create SupervisorJob object and specify it in scope constructor via the overloaded + operator. Read more about combining coroutine context elements here.
Note: if you cancel parent job, you need to create new Job object in order to start new coroutines, that’s why we use cancelChildren.
See also: Avoid cancelling scope job.
var job = SupervisorJob()
val uiScope = CoroutineScope(Dispatchers.Main + job)fun startPresenting() {
loadData()
}fun stopPresenting() {
scope.coroutineContext.cancelChildren()
}fun loadData() = uiScope.launch {
view.showLoading() // ui thread val result = withContext(bgDispatcher) { // background thread
// your blocking call
} view.showData(result) // ui thread
}
lifecycle aware coroutine scope
With a release of android architecture components, we can create lifecycle aware coroutine scope which will cancel itself when Activity#onDestroy
event occurs.
Example of lifecycle aware coroutine scope for LifecycleObserver
.
class MainScope : CoroutineScope, LifecycleObserver {
private val job = SupervisorJob()
override val coroutineContext: CoroutineContext
get() = job + Dispatchers.Main
@OnLifecycleEvent(Lifecycle.Event.ON_PAUSE)
fun destroy() = coroutineContext.cancelChildren()
}// usageclass MainFragment : Fragment() {
private val uiScope = MainScope()
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
lifecycle.addObserver(mainScope)
}
private fun loadData() = uiScope.launch {
val result = withContext(bgDispatcher) {
// your blocking call
}
}
}
Example of lifecycle aware coroutine scope for ViewModel
(author Chris Banes, original source).
open class ScopedViewModel : ViewModel() { private val job = SupervisorJob()
protected val uiScope = CoroutineScope(Dispatchers.Main + job) override fun onCleared() {
super.onCleared()
uiScope.coroutineContext.cancelChildren()
}
}// usageclass MyViewModel : ScopedViewModel() {
private fun loadData() = uiScope.launch {
val result = withContext(bgDispatcher) {
// your blocking call
}
}
}
How to handle exceptions with coroutines
try-catch block
The parent coroutine is launched via the launch
function with the Main
dispatcher.
The child coroutines are launched via the async
function with the IO
dispatcher.
Note: You can use a try-catch
block to catch exceptions and handle them.

To avoid a try-catch
in your presenter class, it's better to handle the exception inside the dataProvider.loadData
function and make it return a generic Result
class.

async parent
The parent coroutine is launched via the async
function with the Main
dispatcher.
Note: To ignore any exceptions, launch the parent coroutine with the async
function.

In this case, the exception will be stored in a Job
object. To retrieve it, you can use the invokeOnCompletion
function.

launch + coroutine exception handler
The parent coroutine is launched via the launch
function with the Main
dispatcher.
Background job is executed via withContext
function with the IO
dispatcher.
Note: You can add a CoroutineExceptionHandler
to the parent coroutine context to catch exceptions and handle them.

How to test coroutines
To launch coroutines you need to specify a CoroutineDispatcher
.

If you want to write unit tests for your MainPresenter
class, you need to make it possible to specify a coroutine context for ui
and background
execution.
Probably the easiest way is to add two parameter to the MainPresenter
constructor: uiDispatcher
with a default value of Main
and ioContext
with a default value of IO
.

Now you can easily test your MainPresenter
class by providing an Unconfined
which will just execute code on the current thread.

How to log coroutine thread
To understand which coroutine performs current work you, can turn on debugging facilities via System.setProperty
and log thread name via Thread.currentThread().name
.

Related articles and special thanks: Guide to coroutines by example, Roman Elizarov, Jake Wharton, Andrey Mischenko.