Writing your own CoroutineContext Element
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.