Android Coroutine Recipes

Dmytro Danylyk
ProAndroidDev
Published in
8 min readOct 9, 2017

--

Table of Contents

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? = null
fun 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 Unconfinedwhich 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.

--

--