Writing your own CoroutineContext Element

Vasiliy Nikitin
MobilePeople
Published in
3 min readSep 9, 2022
https://unsplash.com/photos/WZJyBgHHbJM

What is CoroutineContext.Element?

To understand what an Element of a CoroutineContext is, we should firstly explain what CoroutineContext is on its own. Simply put this is a collection with keys and values, such as a Map, but unlike Map in CoroutineContext key also specifies a type of value. For example, Key<A> can only be associated with an object of type A or its subtypes. So, you can get an Element from the current CoroutineContext in any suspend function by Key, and it may be useful. For example, Room stores a TransactionElement in a CoroutineContext to run database transactions in a dedicated thread.

Overview of some standard elements

Let us briefly explain some most used existing implementations of a CoroutineContext elements:

Job — one of the core instances in a coroutines concept. Represent a background job. Every coroutine is a job itself, in a nested coroutines Job is a parent coroutine.

CoroutineDispatcher — determine how and in which thread coroutine will be executed.

CoroutineExceptionHandler — all exceptions that are not caught in place are propagated up into a CoroutineExceptionHandler. You can add your own exception handler in a CoroutineContext to catch all exceptions.

To add some elements into the existing context you can just use a withContext(..) function because CoroutineContext.Element also, implement a CoroutineContext interface.

Implementation

Let`s imagine such trivial task: we need to execute some long-running tasks, for example, file downloading. On Android, we can do it in the background (using a Service), but if we want to continue the task when a user leaves their device, we should use a WakeLock (of course using a WorkManager should be a better solution in this case but let us forget about this for now).

So, we have a suspend function that downloads a file and must acquire a WakeLock before and release it after. Of course, we can just pass a WakeLock as a parameter to this function, but if we have a chain of functions all of them must pass this parameter through. Another solution is to put it into the CoroutineContext, and we can obtain it in any suspend function running under this context.

Let`s code: the first step is the implementation of WakeLock wrapper so that you can place it in the CoroutineContext:

And then how we can use it:

So now we can put WakeLock into CoroutineContext and use it in this context. This approach also supports nested calls of withWakeLock because our WakeLock is ReferenceCounted — this means it will be released after all acquirers will release it. Note that WakeLock is threaded safe, so we do not need to take care of synchronization.

Usage example:

The behavior of our doLongRunningImportantTask function is context-depended. It can be called without a WakeLockNode in its context without any problems. For example, downloading a large file from a service should be protected by a WakeLock, but this function may also be used for a small file from a ViewModel without it.

--

--