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 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.
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 a result 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.
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:
Job
: Controls the lifecycle of the coroutine.CoroutineDispatcher
: Dispatches work to the appropriate thread.CoroutineName
: The name of the coroutine, useful for debugging.CoroutineExceptionHandler
: Handles uncaught exceptions.
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: