Chapters

Hide chapters

Kotlin Coroutines by Tutorials

Third Edition · Android 12 · Kotlin 1.6 · Android Studio Bumblebee

Section I: Introduction to Coroutines

Section 1: 9 chapters
Show chapters Hide chapters

6. Coroutine Context
Written by Filip Babić

Heads up... You're reading this book for free, with parts of this chapter shown beyond this point as scrambled text.

You’re getting pretty handy with coroutines, aren’t ya? In the previous chapters of this book you’ve seen how you can start coroutines, bridge between threads in coroutines to return values, create your own APIs and much more. In the next few chapters, you’ll focus on the internal coroutine concepts. And the most important is the CoroutineContext. Even though it’s at the core of every coroutine, you’ll see that it’s fairly simple after you take a look at its implementation and usage.

Contextualizing Coroutines

Each coroutine is tied to a CoroutineContext. The context is a wrapper around a set of CoroutineContext.Elements, each of which describes a vital part that builds up and forms a coroutine, the way exceptions are propagated and execution flow is navigated or just the general lifecycle.

These elements are:

  • Job: A cancellable piece of work, which has a defined lifecycle.
  • ContinuationInterceptor: A mechanism which listens to the continuation within a coroutine and intercepts its resumption.
  • CoroutineExceptionHandler: A construct which handles exceptions in coroutines.

So, when you run launch, you can pass it a context of your own choice. The context defines which elements will fit into the puzzle. If you pass in a Job, which implements CoroutineContext.Element, you’ll define what the new coroutine’s parent is. As such, if the parent job finishes, it will notify all of its children, including the newly created coroutine.

If you pass in an exception handler, another CoroutineContext.Element, you give the coroutine a way to process errors if something bad happens.

And the last thing you can pass in is a ContinuationInterceptor. These constructs control the flow of each coroutine-powered function, by determining which thread it should operate on and how it should distribute work.

You wouldn’t want to write a full implementation that manually handles continuations. If you want something to do that for you, while also being a CoroutineContext.Element, you have to provide a coroutine dispatcher.

You’ve used some of them before — like Dispatchers.Default. So the key to understanding ContinuationInterceptor usage is by learning what a dispatcher really is, which you’ll do in “Chapter 7: Context Switch & Dispatching”. For now, you’ll focus on combining and providing CoroutineContexts.

Using CoroutineContext

To follow the code in this chapter, open this chapter’s starter project using IntelliJ by selecting Open Project. Then navigate to the coroutine-context/projects/starter folder, selecting the coroutine-context project.

GlobalScope.launch {
  println("In a coroutine")
}
public fun CoroutineScope.launch(
    context: CoroutineContext = EmptyCoroutineContext,
    start: CoroutineStart = CoroutineStart.DEFAULT,
    block: suspend CoroutineScope.() -> Unit
): Job

Combining Different Contexts

Another interesting aspect to coroutine contexts is the ability to compose them and combine their functionality. Using the +/plus operator, you can create a new CoroutineContext from the combination of the two. Since you know each coroutine is composed of several objects, like the continuation interceptor for the threading, exception handler for errors and a Job for lifecycle, there has to be a way to create a new coroutine with all these pieces of the puzzle. And this is where summing contexts comes in handy. You can do it as simply as this:

fun main() {
  val defaultDispatcher = Dispatchers.Default

  val coroutineErrorHandler = CoroutineExceptionHandler { context, error ->
    println("Problems with Coroutine: $error") // we just print the error here
  }

  val emptyParentJob = Job()

  val combinedContext = defaultDispatcher + coroutineErrorHandler + emptyParentJob

  GlobalScope.launch(context = combinedContext) {
    println(Thread.currentThread().name)
  }

  Thread.sleep(50)
}
DefaultDispatcher-worker-1

Process finished with exit code 0

Providing Contexts

When it comes to software, you usually want to build it in a way that abstracts away the communication between layers. With threading, it’s useful to abstract the way you switch between different threads. You can abstract this by attaching a thread provider, providing both main and background threads. It’s no different with coroutines!

Building the ContextProvider

You’ve already learned which CoroutineContext objects exist and what their behavior is. To build the provider, you first have to define an interface, which provides a generic context, which you’ll run the expensive operations on. Note that this Provider interface is not part of Coroutines API but will help us abstract out the main and background contexts. The interface would look like this:

interface CoroutineContextProvider {

  fun context(): CoroutineContext
}
class CoroutineContextProviderImpl(
    private val context: CoroutineContext
) : CoroutineContextProvider {

  override fun context(): CoroutineContext = context
}
fun main() {
  val parentJob = Job()
  val provider: CoroutineContextProvider = CoroutineContextProviderImpl(
    context = parentJob + Dispatchers.IO
  )

  GlobalScope.launch(context = provider.context()) {
    println(Thread.currentThread().name)
  }

  Thread.sleep(50)
}
val mainContextProvider: CoroutineContextProvider = 
  CoroutineContextProviderImpl(Dispatchers.Main)

Key Points

  • All the information for coroutines is contained in a CoroutineContext and its CoroutineContext.Elements.
  • There are three main coroutine context elements: the Job, which defines the lifecycle and can be cancelled, a CoroutineExceptionHandler, which takes care of errors, and the ContinuationInterceptor, which handles function execution flow and threading.
  • Each of the coroutine context elements implements CoroutineContext.
  • ContinuationInterceptors, which take care of the input/output of threading. The main and background threads are provided through the Dispatchers.
  • You can combine different CoroutineContexts and their Elements by using the +/plus operator, effectively summing their elements.
  • A good practice is to build a CoroutineContext provider, so you don’t depend on explicit contexts.
  • With the CoroutineContextProvider you can abstract away complex contexts, like custom error handling, coroutine lifecycles or threading mechanisms.
  • The CoroutineContextProvider is very useful in testing as you can abstract away the context that is specific to the testing environment.
Have a technical question? Want to report a bug? You can ask questions and report bugs to the book authors in our official book forum here.
© 2024 Kodeco Inc.

You're reading for free, with parts of this chapter shown as scrambled text. Unlock this book, and our entire catalogue of books and videos, with a Kodeco Personal Plan.

Unlock now