Improve app performance with Kotlin coroutines

A coroutine is a concurrency design pattern that you can use on Android to simplify code that executes asynchronously. Coroutines were added to Kotlin in version 1.3 and are based on established concepts from other languages.

On Android, coroutines help to solve two primary problems:

  • Manage long-running tasks that might otherwise block the main thread and cause your app to freeze.
  • Providing main-safety, or safely calling network or disk operations from the main thread.

This topic describes how you can use Kotlin coroutines to address these problems, enabling you to write cleaner and more concise app code.

Manage long-running tasks

On Android, every app has a main thread that handles the user interface and manages user interactions. If your app is assigning too much work to the main thread, the app can seemingly freeze or slow down significantly. Network requests, JSON parsing, reading or writing from a database, or even just iterating over large lists can cause your app to run slowly enough to cause visible jank—slow or frozen UI that responds slowly to touch events. These long-running operations should run outside of the main thread.

The following example shows simple coroutines implementation for a hypothetical long-running task:

suspend fun fetchDocs() {                             // Dispatchers.Main
    val result = get("https://developer.android.com") // Dispatchers.IO for `get`
    show(result)                                      // Dispatchers.Main
}

suspend fun get(url: String) = withContext(Dispatchers.IO) { /* ... */ }

Coroutines build upon regular functions by adding two operations to handle long-running tasks. In addition to invoke (or call) and return, coroutines add suspend and resume:

  • suspend pauses the execution of the current coroutine, saving all local variables.
  • resume continues execution of a suspended coroutine from the place where it was suspended.

You can call suspend functions only from other suspend functions or by using a coroutine builder such as launch to start a new coroutine.

In the example above, get() still runs on the main thread, but it suspends the coroutine before it starts the network request. When the network request completes, get resumes the suspended coroutine instead of using a callback to notify the main thread.

Kotlin uses a stack frame to manage which function is running along with any local variables. When suspending a coroutine, the current stack frame is copied and saved for later. When resuming, the stack frame is copied back from where it was saved, and the function starts running again. Even though the code might look like an ordinary sequential blocking request, the coroutine ensures that the network request avoids blocking the main thread.

Use coroutines for main-safety

Kotlin coroutines use dispatchers to determine which threads are used for coroutine execution. To run code outside of the main thread, you can tell Kotlin coroutines to perform work on either the Default or IO dispatcher. In Kotlin, all coroutines must run in a dispatcher, even when they're running on the main thread. Coroutines can suspend themselves, and the dispatcher is responsible for resuming them.

To specify where the coroutines should run, Kotlin provides three dispatchers that you can use:

  • Dispatchers.Main - Use this dispatcher to run a coroutine on the main Android thread. This should be used only for interacting with the UI and performing quick work. Examples include calling suspend functions, running Android UI framework operations, and updating LiveData objects.
  • Dispatchers.IO - This dispatcher is optimized to perform disk or network I/O outside of the main thread. Examples include using the Room component, reading from or writing to files, and running any network operations.
  • Dispatchers.Default - This dispatcher is optimized to perform CPU-intensive work outside of the main thread. Example use cases include sorting a list and parsing JSON.

Continuing the previous example, you can use the dispatchers to re-define the get function. Inside the body of get, call withContext(Dispatchers.IO) to create a block that runs on the IO thread pool. Any code you put inside that block always executes via the IO dispatcher. Since withContext is itself a suspend function, the function get is also a suspend function.

suspend fun fetchDocs() {                      // Dispatchers.Main
    val result = get("developer.android.com")  // Dispatchers.Main
    show(result)                               // Dispatchers.Main
}

suspend fun get(url: String) =                 // Dispatchers.Main
    withContext(Dispatchers.IO) {              // Dispatchers.IO (main-safety block)
        /* perform network IO here */          // Dispatchers.IO (main-safety block)
    }                                          // Dispatchers.Main
}

With coroutines, you can dispatch threads with fine-grained control. Because withContext() lets you control the thread pool of any line of code without introducing callbacks, you can apply it to very small functions like reading from a database or performing a network request. A good practice is to use withContext() to make sure every function is main-safe, which means that you can call the function from the main thread. This way, the caller never needs to think about which thread should be used to execute the function.

In the previous example, fetchDocs() executes on the main thread; however, it can safely call get, which performs a network request in the background. Because coroutines support suspend and resume, the coroutine on the main thread is resumed with the get result as soon as the withContext block is done.

Performance of withContext()

withContext() does not add extra overhead compared to an equivalent callback-based implementation. Furthermore, it's possible to optimize withContext() calls beyond an equivalent callback-based implementation in some situations. For example, if a function makes ten calls to a network, you can tell Kotlin to switch threads only once by using an outer withContext(). Then, even though the network library uses withContext() multiple times, it stays on the same dispatcher and avoids switching threads. In addition, Kotlin optimizes switching between Dispatchers.Default and Dispatchers.IO to avoid thread switches whenever possible.

Designate a CoroutineScope

When defining a coroutine, you must also designate its CoroutineScope. A CoroutineScope manages one or more related coroutines. You can also use a CoroutineScope to start a new coroutine within that scope. Unlike a dispatcher, however, a CoroutineScope doesn't run the coroutines.

One important function of CoroutineScope is stopping coroutine execution when a user leaves a content area within your app. Using CoroutineScope, you can ensure that any running operations stop correctly.

Use CoroutineScope with Android Architecture components

On Android, you can associate CoroutineScope implementations with a component lifecycle. This lets you avoid leaking memory or doing extra work for activities or fragments that are no longer relevant to the user. Using Jetpack components, they fit naturally in a ViewModel. Because a ViewModel isn't destroyed during configuration changes (such as screen rotation), you don't have to worry about your coroutines getting canceled or restarted.

Scopes know about every coroutine that they start. This means that you can cancel everything that was started in the scope at any time. Scopes propagate themselves, so if a coroutine starts another coroutine, both coroutines have the same scope. This means that even if other libraries start a coroutine from your scope, you can cancel them at any time. This is particularly important if you’re running coroutines in a ViewModel. If your ViewModel is being destroyed because the user has left the screen, all the asynchronous work that it is doing must be stopped. Otherwise, you’ll waste resources and potentially leak memory. If you have asynchronous work that should continue after you destroy your ViewModel, it should be done in a lower layer of your app’s architecture.

With the KTX library for Android Architecture components, you can also use an extension property, viewModelScope, to create coroutines that can run until the ViewModel is destroyed.

Start a coroutine

You can start coroutines in one of two ways:

  • launch starts a new coroutine and doesn't return the result to the caller. Any work that is considered "fire and forget" can be started using launch.
  • async starts a new coroutine and allows you to return a result with a suspend function called await.

Typically, you should launch a new coroutine from a regular function, as a regular function cannot call await. Use async only when inside another coroutine or when inside a suspend function and performing parallel decomposition.

Building on the previous examples, here's a coroutine with the viewModelScope KTX extension property that uses launch to switch from regular functions to coroutines:

fun onDocsNeeded() {
    viewModelScope.launch {    // Dispatchers.Main
        fetchDocs()            // Dispatchers.Main (suspend function call)
    }
}

Parallel decomposition

All coroutines that are started by a suspend function must be stopped when that function returns, so you likely need to guarantee that those coroutines finish before returning. With structured concurrency in Kotlin, you can define a coroutineScope that starts one or more coroutines. Then, using await() (for a single coroutine) or awaitAll() (for multiple coroutines), you can guarantee that these coroutines finish before returning from the function.

As an example, let's define a coroutineScope that fetches two documents asynchronously. By calling await() on each deferred reference, we guarantee that both async operations finish before returning a value:

suspend fun fetchTwoDocs() =
    coroutineScope {
        val deferredOne = async { fetchDoc(1) }
        val deferredTwo = async { fetchDoc(2) }
        deferredOne.await()
        deferredTwo.await()
    }

You can also use awaitAll() on collections, as shown in the following example:

suspend fun fetchTwoDocs() =        // called on any Dispatcher (any thread, possibly Main)
    coroutineScope {
        val deferreds = listOf(     // fetch two docs at the same time
            async { fetchDoc(1) },  // async returns a result for the first doc
            async { fetchDoc(2) }   // async returns a result for the second doc
        )
        deferreds.awaitAll()        // use awaitAll to wait for both network requests
    }

Even though fetchTwoDocs() launches new coroutines with async, the function uses awaitAll() to wait for those launched coroutines to finish before returning. Note, however, that even if we had not called awaitAll(), the coroutineScope builder does not resume the coroutine that called fetchTwoDocs until after all of the new coroutines completed.

In addition, coroutineScope catches any exceptions that the coroutines throw and routes them back to the caller.

For more information on parallel decomposition, see Composing suspending functions.

Architecture components with built-in support

Some Architecture components, including ViewModel and Lifecycle, include built-in support for coroutines through their own CoroutineScope members.

For example, ViewModel includes a built-in viewModelScope. This provides a standard way to launch coroutines within the scope of the ViewModel, as shown in the following example:

class MyViewModel : ViewModel() {

    fun launchDataLoad() {
        viewModelScope.launch {
            sortList()
            // Modify UI
        }
    }

    /**
    * Heavy operation that cannot be done in the Main Thread
    */
    suspend fun sortList() = withContext(Dispatchers.Default) {
        // Heavy work
    }
}

LiveData also utilizes coroutines with a liveData block:

liveData {
    // runs in its own LiveData-specific scope
}

For more information on Architecture components with built-in coroutines support, see Use Kotlin coroutines with Architecture components.

More information

For more coroutines-related information, see the following links: