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 updatingLiveData
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 usinglaunch
.async
starts a new coroutine and allows you to return aresult
with a suspend function calledawait
.
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: