Coroutines: a practical vocabulary
Coroutines is a powerful feature that makes running async work easy in Kotlin.
It's quite easy to get started with coroutines and running work in the non-main thread by copying-pasting code from the web (and most of the time it's fine).
If you already did that but you would like to understand the bare basics about those context, scope and other objects you are using you are reading the right blog post :)
Coroutine Context
A coroutine executes always in a Context. The context is a group of other objects, most notably the Job and Dispatcher which are explained below.
Job
Represents a background job. It's cancellable, and you can arrange jobs in parent-child hierarchies.
When you launch a coroutine within a coroutine, it inherits the Coroutine Context. The Job of the new coroutine becomes child of the parent's coroutine Job. Therefore, when the parent coroutine is canceled, all its children are recursively canceled too.
Dispatcher
Determines which thread(s) the coroutine uses for the execution.
When launching a child coroutine (as seen above), you can override the Dispatcher by passing it to the launch()
method.
To change Dispatcher within the same coroutine, use withContext()
(the context inside the withContext
block is the existing context plus the provided).
Coroutine scope
Essentially a class that contains a Coroutine context. It exists to manage the lifecycle of coroutines (without manipulating contexts and jobs manually).
But why have another class if it just contains the context? tl;dr: Because they serve a different purpose (longer explanation).
Most of the time, the scope is attached to objects that we want to stop all coroutines they launched when they stop to exist (e.g. Activities, ViewModels, etc).
All the coroutine builders methods you've seen (e.g. launch
, async
) are extension methods of Coroutine context.
To use a scope:
- Create it (and manage it) manually:
class Activity {
private val mainScope = MainScope()
fun destroy() {
mainScope.cancel()
}
fun doSomething() {
mainScope.launch {
[...]
}
}
}
- Use delegation:
class Activity : CoroutineScope by CoroutineScope(Dispatchers.Default) {
fun doSomething() {
// Note that the scope is implied here
launch {
[...]
}
}
}
- Use provided scope:
class MyViewModel : ViewModel() {
fun doSomething() {
// Android Jetpack ViewModel, provide a scope attached to VM lifecycle
viewModelScope.launch {
[...]
}
}
}
Hopefully, these coroutine related concepts are a bit more clear now!