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. Let's check out its definition:
It seems to be just a wrapper around
CoroutineContext. Then you might remind yourself how
Continuation is defined.
CoroutineContext as well. This type is used by the most important Kotlin coroutine elements. It must be a really important concept. So what is it?
CoroutineContext is an interface, representing 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 a
CoroutineContext as well. So it is like every element in the collection, is a collection by itself. This is done this way, to allow convenient context specification and modification, as in the below example (adding contexts and setting coroutine builder context will be explained later).
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
CoroutineExceptionHandler and dispatchers from the
Dispatchers object. Those 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 getter. In Kotlin
get method is an operator and can be invoked using square brackets instead. Just like in a 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, when contexts like
CoroutineNameare part of the kotlinx.coroutines library, so they needs to be imported from
To find a
CoroutineName, we used just
CoroutineName. This is not a type, or a class, this is the companion object. It is a Kotlin feature, 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 a common practice in kotlinx.coroutines library, to use companion objects as keys to the elements with the same name. It makes it easier to remember2. The element might be a class (like
CoroutineName) or it might be an interface (like
Job) implemented by many classes acts as elements 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 one that is empty. Such context by itself returns no elements and does not modify another context if added.
Elements can also be removed from a context by key using the
minusis not overloaded for
CoroutineContext. I believe it is because its meaning would not be clear enough, as explained in the Effective Kotlin Item 12: Operator meaning should be consistent with its function name.
If we need to do something for each element in the context, we can use the
fold method for that. It is similar to
fold on 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 on.
Coroutine context and builders
CoroutineContext is just a way to hold and pass data. Since some of its elements are mutable (
Job for instance), we can let them communicate with each other. For instance, such communication happens between a parent and a child coroutine. 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.
A simplified formula to calculate a coroutine context is:
Since the new elements with the same key always replace the old ones, the child context always overrides the 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
CoroutineId when the application is in debug mode.
Creating our own context
It is not a common need, but we might create our own coroutine context pretty easily. To do that, the easiest way is to create a class that implements the
CoroutineContext.Element interface. Such class needs a property
key of type
CoroutineContext.Key<*>. This key will be used as a key identifying this context. The common practice is to use this class 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 one of the same type. To see it in practice, below you can see how an example context printing next numbers works in action.
I have seen custom contexts in use as a kind of dependency injection - to easily inject different values on production and different on tests. Although I don't think it will become standard practice.
We do not really need to have
CoroutineScope to access context. As you might remember from the chapter Coroutines under the hood, context is referenced by continuations, which are passed to each suspending function. The suspending function context can be referenced using
coroutineContext suspending extension property.
This also means that the
printNext function from the
CounterContext example can be implemented just as a suspending function (instead of as an extension on
CoroutineContext is conceptually similar to a map or a set collection. It is an indexed set of
Element instances, where each
Element is a
CoroutineContext as well. 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. Those objects are kept by the coroutines, and can determine how those coroutines should be running (what is their state, at what thread, etc). In the next chapters, we will discuss the most essential coroutine contexts in the Kotlin coroutines library.
Let’s clear the nomenclature.
launch is an extension function on
CoroutineContext is its receiver type. Extension function receiver is the object we use
this to reference to.
The below companion object is named
Key. It is possible but changes little in terms of how it is used. The default companion object name is
Companion, and so this name is used when we need to reference it using reflection or when we define an extension function on it. Here we use