The original

Coroutines are a concurrent design pattern that you can use on Android to simplify code that executes asynchronously. Kotlin version 1.3 adds Coroutines and builds on established concepts in other languages.

On Android, coroutines help solve two major problems:

  • Manage long-running tasks that might otherwise block the main thread and freeze applications.
  • Provides primary security, or securely invoking network or disk operations from the main thread.

This topic describes how to use Kotlin coroutines to solve these problems, enabling you to write clearer, more concise application code.

Manage long-running tasks

On Android, each application has a main thread that handles the user interface and manages user interaction. If your application has too much work assigned to the main thread, the application may be significantly stuttering or slow. Network requests, JSON parsing, reading or writing from a database, or even just iterating over large lists can cause your application to run slowly, resulting in a visible slow or frozen UI that is slow to respond to touch events. These long-running operations should run outside the main thread.

The following example shows a simple coroutine implementation of a hypothetical long-running task:

suspend fun fetchDocs(a) {               // 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) { / *... * / }
Copy the code

Coroutines build regular functionality by adding two operations to handle long-running tasks. In addition to invoke (or call) and return, the coroutine adds suspend and resume:

  • suspendPauses execution of the current coroutine, saving all local variables.
  • resumeResuming a suspended coroutine that continues execution from the suspended coroutine.

You can only call the suspend function from another suspend function or start a new coroutine using a coroutine builder such as start.

In the example above, get() is still running on the main thread, but it suspends the coroutine before starting the network request. Instead of using callbacks to notify the main thread, GET resumes the suspended coroutine when the network request completes.

Kotlin uses a stack framework to manage functions that run with any local variables. When the coroutine is suspended, the current stack frame is copied and saved for later use. When restored, the stack frame will be copied back from the saved position and the function will start running again. Even if the code looks like normal sequential blocking requests, coroutines ensure that network requests avoid blocking the main thread.

Use coroutines to secure the main thread

Kotlin coroutines use the scheduler to determine which threads are used for coroutine execution. To run code outside of the main thread, you can tell Kotlin that the coroutine performs work on the Default or IO scheduler. In Kotlin, all coroutines must run in the scheduler, even if they are running on the main thread. Coroutines can be paused, and the scheduler is responsible for restoring them.

To specify where the coroutine should run, Kotlin provides three schedulers that can be used:

  • Dispatchers.Main – Use this scheduler to run the coroutine on the Main Android thread. This should only be used to interact with the UI and perform fast work. Examples include calling suspend functions, running Android UI framework actions, and updatesLiveDataObject.
  • Dispatchers.IO – This scheduler has been optimized to perform disk or network I/O outside the main thread. Examples include using the Room component, reading or writing files, and running any network operation.
  • Dispatchers.Default – This scheduler has been optimized to perform CPU intensive work outside of the main thread. Example use cases include sorting lists and parsing JSON.

Continuing with the previous example, you can use the scheduler to redefine the GET function. Within the body of get, withContext(Dispatchers.io) is called to create a block to run on the IO thread pool. Any code placed in this block is always executed through the IO scheduler. Since withContext is itself a suspend function, the get function is also a suspend function.

With coroutines, you can schedule threads with fine-grained control. Because withContext() allows you to control the thread pool of any line of code without introducing callbacks, you can apply it to very small functions, such as reading from a database or performing network requests. A good practice is to use withContext() to ensure that each function is primary safe, which means you can call the function from the main thread. This way, the caller never needs to consider which thread should be used to execute the function.

In the previous example, fetchDocs() executes on the main thread; However, it can safely invoke GET, which performs network requests in the background. Because coroutines support suspend and restore, coroutines on the main thread are restored with get results as soon as the withContext block completes.

Important: Using suspend does not tell Kotlin to run a function on a background thread. The pause function runs normally on the main thread. It is also common to start coroutines on the main thread. When you need primary security, such as when reading or writing to a disk, performing network operations, or running CPU-intensive operations, always use withContext() inside the suspend function.

WithContext () does not add any additional overhead compared to the equivalent callback-based implementation. In addition, in some cases, the withContext() call can be optimized rather than based on an equivalent callback-based implementation. For example, if a function makes ten calls to the network, you can tell Kotlin to switch threads only once by using the external withContext(). Then, even if the network library uses withContext() multiple times, it stays on the same scheduler and avoids switching threads. In addition, Kotlin optimizes the switch between dispatchers. Default and dispatchers. IO to avoid thread switching as much as possible.

Important: Using Dispatchers that use thread pools such as dispatchers. IO or dispatchers. Default does not guarantee that the block will execute on the same thread from top to bottom. In some cases, the Kotlin coroutine may move execution to another thread after pausing and resuming. This means thread-local variables may not point to the same value for the entire withContext() block.

Specify CoroutineScope

When you define a coroutine, you must also specify its CoroutineScope. The CoroutineScope manages one or more related coroutines. You can also use the CoroutineScope to start a new coroutine in this range. Unlike the scheduler, however, the CoroutineScope does not run the coroutine.

An important feature of CoroutineScope is to stop coroutine execution when the user leaves the content area in the application. Using the CoroutineScope, you can ensure that any running operation is stopped correctly.

Use CoroutineScope with Android architecture components

On Android, you can associate the CoroutineScope implementation with the component lifecycle. This avoids leaking memory or doing extra work for activities or fragments that are no longer relevant to the user. With Jetpack components, they fit naturally into the ViewModel. Because the ViewModel is not destroyed during configuration changes, such as screen rotation, you don’t have to worry about the coroutine being canceled or restarted.

Scopes know every coroutine they start. This means that you can cancel anything launched in scope at any time. Scope propagates itself, so if one coroutine starts another coroutine, both coroutines have the same scope. This means that even if other libraries start coroutines from your scope, you can cancel them at any time. This is especially important if you are running coroutines in the ViewModel. If the ViewModel is destroyed because the user has left the screen, you must stop all asynchronous work it is performing. Otherwise, you will waste resources and possibly leak memory. If you should continue to work asynchronously after destroying the ViewModel, you should do so in the lower layers of the application architecture.

Warning: Cancel the coroutine by throwing a CancellationException. An exception handler that catches an exception or Throwable is triggered during coroutine cancellation.

Using the KTX library component for the Android architecture, you can also use the extended property viewModelScope to create a coroutine that can run until the ViewModel is destroyed.

Start a coroutine

You can start the coroutine in one of two ways:

  • launchA new coroutine is started and the result is not returned to the caller. Any job that is considered “launch and forget” can be usedlaunchTo get started.
  • asyncLaunch a new coroutine and allow you to use the nameawaitReturns the result of the suspension function.

In general, you should start new coroutines from regular functions, because regular functions cannot call wait. Asynchrony is used only when parallel decomposition is performed within another coroutine or within a suspended function.

Building on the previous example, here is a coroutine with the viewModelScope KTX extension property that uses launch to switch from a regular function to a coroutine:

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

Warning: Start and asynchronously handle exceptions differently. Since async expects to finally call await at some point, it retains the exceptions and rethrows them in the await call. This means that if you start a new coroutine from a regular function with await, the exception may be silently removed. These discarded exceptions do not appear in the crash indicator, nor in logcat.

Parallel decomposition

When the function returns, all coroutines started by the suspended function must be stopped, so you may need to ensure that these coroutines complete before returning. With structured concurrency in Kotlin, you can define a coroutineScope that starts one or more coroutines. Then, with await() (for a single coroutine) or awaitAll() (for multiple coroutines), it is guaranteed that these coroutines will complete before returning from the function.

For example, let’s define a coroutineScope that retrieves two documents asynchronously. By calling await() on each deferred reference, we guarantee that both asynchronous operations are completed before the value is returned:

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

Even if fetchTwoDocs() starts new coroutines asynchronously, the function uses awaitAll () to wait for those started coroutines to complete before returning. Note, however, that even if we do not call awaitAll (), the coroutineScope builder will not resume the coroutines calling fetchTwoDocs until all new coroutines are complete.

In addition, the coroutineScope catches any exceptions thrown by the coroutine and routes them back to the caller.

For more information about parallel decomposition, see Writing suspend functions.

Architectural components with built-in support

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

For example, the ViewModel contains a built-in viewModelScope. A standard way to start coroutines in ViewModel scope is provided, as shown in the following example:

class MyViewModel : ViewModel() {

  fun launchDataLoad(a) {
    viewModelScope.launch {
      sortList()
      // Modify UI}}/** * Heavy operation that cannot be done in the Main Thread */
  suspend fun sortList(a) = withContext(Dispatchers.Default) {
    // Heavy work}}Copy the code

LiveData also uses coroutines with LiveData blocks:

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