What is CoroutineContext and how does it work?
If you take a look at the coroutine builders’ definitions, you will see that their first parameter is of type
The receiver and the last argument’s receiver are of type
CoroutineScope seems to be an important concept, so let's check out its definition:
It seems to be just a wrapper around
CoroutineContext. So, you might want to recall how
Continuation is defined.
CoroutineContext as well. This type is used by the most important Kotlin coroutine elements. This must be a really important concept, so what is it?
CoroutineContext is an interface that represents an element or a collection of elements. It is conceptually similar to a map or a set collection: it is an indexed set of
Element instances like
CouroutineDispatcher, etc. The unusual thing is that each
Element is also a
CoroutineContext. So, every element in a collection is a collection in itself.
This concept is quite intuitive. Imagine a mug. It is a single element, but it is also a collection that contains a single element. When you add another mug, you have a collection with two elements.
In order to allow convenient context specification and modification, each
CoroutineContext element is a
CoroutineContext itself, as in the example below (adding contexts and setting a coroutine builder context will be explained later). Just specifying or adding contexts is much easier than creating an explicit set.
Every element in this set has a unique
Key that is used to identify it. These keys are compared by reference.
CoroutineContext.Element, which implements the
It’s the same with
CoroutineExceptionHandler and dispatchers from the
Dispatchers object. These are the most important coroutine contexts. They will be explained in the next chapters.
Finding elements in CoroutineContext
CoroutineContext is like a collection, we can find an element with a concrete key using
get. Another option is to use square brackets, because in Kotlin the
get method is an operator and can be invoked using square brackets instead of an explicit function call. Just like in
Map: when an element is in the context, it will be returned. If it is not,
null will be returned instead.
CoroutineContextis part of the built-in support for Kotlin coroutines, so it is imported from
kotlin.coroutines, while contexts like
CoroutineNameare part of the kotlinx.coroutines library, so they need to be imported from
To find a
CoroutineName, we use just
CoroutineName. This is not a type or a class: it is a companion object. It is a feature of Kotlin that a name of a class used by itself acts as a reference to its companion object, so
ctx[CoroutineName] is just a shortcut to
It is common practice in the kotlinx.coroutines library to use companion objects as keys to elements with the same name. This makes it easier to remember2. A key might point to a class (like
CoroutineName) or to an interface (like
Job) that is implemented by many classes with the same key (like
CoroutineContext truly useful is the ability to merge two of them together.
When two elements with different keys are added, the resulting context responds to both keys.
When another element with the same key is added, just like in a map, the new element replaces the previous one.
Empty coroutine context
CoroutineContext is like a collection, we also have an empty context. Such a context by itself returns no elements; if we add it to another context, it behaves exactly like this other context.
Elements can also be removed from a context by their key using the
minusoperator is not overloaded for
CoroutineContext. I believe this is because its meaning would not be clear enough, as explained in Effective Kotlin Item 12: An operator’s meaning should be consistent with its function name.
If we need to do something for each element in a context, we can use the
fold method, which is similar to
fold for other collections. It takes:
- an initial accumulator value;
- an operation to produce the next state of the accumulator, based on the current state, and the element it is currently invoked in.
Coroutine context and builders
CoroutineContext is just a way to hold and pass data. By default, the parent passes its context to the child, which is one of the parent-child relationship effects. We say that the child inherits context from its parent.
Each child might have a specific context defined in the argument. This context overrides the one from the parent.
A simplified formula to calculate a coroutine context is:
Since new elements always replace old ones with the same key, the child context always overrides elements with the same key from the parent context. The defaults are used only for keys that are not specified anywhere else. Currently, the defaults only set
Dispatchers.Default when no
ContinuationInterceptor is set, and they only set
CoroutineId when the application is in debug mode.
There is a special context called
Job, which is mutable and is used to communicate between a coroutine’s child and its parent. The next chapters will be dedicated to the effects of this communication.
Accessing context in a suspending function
CoroutineScope has a
coroutineContext property that can be used to access the context. But what if we are in a regular suspending function? As you might remember from the Coroutines under the hood chapter, context is referenced by continuations, which are passed to each suspending function. So, it is possible to access a parent’s context in a suspending function. To do this, we use the
coroutineContext property, which is available in every suspending scope.
Creating our own context
It is not a common need, but we can create our own coroutine context pretty easily. To do this, the easiest way is to create a class that implements the
CoroutineContext.Element interface. Such a class needs a property
key of type
CoroutineContext.Key<*>. This key will be used as the key that identifies this context. The common practice is to use this class’s companion object as a key. This is how a very simple coroutine context can be implemented:
Such a context will behave a lot like
CoroutineName: it will propagate from parent to child, but any children will be able to override it with a different context with the same key. To see this in practice, below you can see an example context that is designed to print consecutive numbers.
I have seen custom contexts in use as a kind of dependency injection - to easily inject different values in production than in tests. However, I don't think this will become standard practice.
CoroutineContext is conceptually similar to a map or a set collection. It is an indexed set of
Element instances, where each
Element is also a
CoroutineContext. Every element in it has a unique
Key that is used to identify it. This way,
CoroutineContext is just a universal way to group and pass objects to coroutines. These objects are kept by the coroutines and can determine how these coroutines should be running (what their state is, in which thread, etc). In the next chapters, we will discuss the most essential coroutine contexts in the Kotlin coroutines library.
Let’s clear up the nomenclature.
launch is an extension function on
CoroutineScope is its receiver type. The extension function’s receiver is the object we reference with
The companion object below is named
Key. We can name companion objects, but this changes little in terms of how they are used. The default companion object name is
Companion, so this name is used when we need to reference this object using reflection or when we define an extension function on it. Here we use