Improve app performance with Kotlin coroutines

Kotlin coroutines enable you to write clean, simplified asynchronous code that keeps your app responsive while managing long-running tasks such as network calls or disk operations.

This topic provides a detailed look at coroutines on Android. If you're unfamiliar with coroutines, be sure to read Kotlin coroutines on Android before reading this topic.

Manage long-running tasks

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.

The following example shows a simple coroutine 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) { /* ... */ }

In this example, 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.

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.

Parallel decomposition

All coroutines that are started inside 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.

Coroutines concepts

CoroutineScope

A CoroutineScope keeps track of any coroutine it creates using launch or async. The ongoing work (i.e. the running coroutines) can be cancelled by calling scope.cancel() at any point in time. In Android, some KTX libraries provide their own CoroutineScope for certain lifecycle classes. For example, ViewModel has a viewModelScope, and Lifecycle has lifecycleScope. Unlike a dispatcher, however, a CoroutineScope doesn't run the coroutines.

viewModelScope is also used in the examples found in Background threading on Android with Coroutines. However, if you need to create your own CoroutineScope to control the lifecycle of coroutines in a particular layer of your app, you can create one as follows:

class ExampleClass {

    // Job and Dispatcher are combined into a CoroutineContext which
    // will be discussed shortly
    val scope = CoroutineScope(Job() + Dispatchers.Main)

    fun exampleMethod() {
        // Starts a new coroutine within the scope
        scope.launch {
            // New coroutine that can call suspend functions
            fetchDocs()
        }
    }

    fun cleanUp() {
        // Cancel the scope to cancel ongoing coroutines work
        scope.cancel()
    }
}

A cancelled scope cannot create more coroutines. Therefore, you should call scope.cancel() only when the class that controls its lifecycle is being destroyed. When using viewModelScope, the ViewModel class cancels the scope automatically for you in the ViewModel's onCleared() method.

Job

A Job is a handle to a coroutine. Each coroutine that you create with launch or async returns a Job instance that uniquely identifies the coroutine and manages its lifecycle. You can also pass a Job to a CoroutineScope to further manage its lifecycle, as shown in the following example:

class ExampleClass {
    ...
    fun exampleMethod() {
        // Handle to the coroutine, you can control its lifecycle
        val job = scope.launch {
            // New coroutine
        }

        if (...) {
            // Cancel the coroutine started above, this doesn't affect the scope
            // this coroutine was launched in
            job.cancel()
        }
    }
}

CoroutineContext

A CoroutineContext defines the behavior of a coroutine using the following set of elements:

For new coroutines created within a scope, a new Job instance is assigned to the new coroutine, and the other CoroutineContext elements are inherited from the containing scope. You can override the inherited elements by passing a new CoroutineContext to the launch or async function. Note that passing a Job to launch or async has no effect, as a new instance of Job is always assigned to a new coroutine.

class ExampleClass {
    val scope = CoroutineScope(Job() + Dispatchers.Main)

    fun exampleMethod() {
        // Starts a new coroutine on Dispatchers.Main as it's the scope's default
        val job1 = scope.launch {
            // New coroutine with CoroutineName = "coroutine" (default)
        }

        // Starts a new coroutine on Dispatchers.Default
        val job2 = scope.launch(Dispatchers.Default + CoroutineName("BackgroundCoroutine")) {
            // New coroutine with CoroutineName = "BackgroundCoroutine" (overridden)
        }
    }
}

Additional coroutines resources

For more coroutines resources, see the following links: