Stronger Typing for CoroutineScopes
Don't guess about what kind of CoroutineScope you have.

If you use Kotlin coroutines, you’re probably familiar with the Dispatchers
object:
object Dispatchers {
val Default: CoroutineDispatcher = createDefaultDispatcher()
val Main: MainCoroutineDispatcher get() = MainDispatcherLoader.dispatcher
val Unconfined: CoroutineDispatcher = kotlinx.coroutines.Unconfined
val IO: CoroutineDispatcher = DefaultScheduler.IO
}
And you’re probably aware that you can define a default dispatcher when you’re implementing or creating a CoroutineScope
:
class MyCoroutineScope : CoroutineScope {
override val coroutineContext = Job() + Dispatchers.Main
}
val someScope = CoroutineScope(Job() + Dispatchers.IO)
If you don’t specify a dispatcher, Dispatchers.Default
is used.
Note that I believe it’s generally a bad idea to have a class implement CoroutineScope
. You can read more of my thoughts on the subject in my last article here.
This flexibility is nice to have, but I think it’s a bit too much at times. For instance, when you’re providing a CoroutineScope
to a UI class, you probably want to make sure it always defaults to Main
. If it's some class which does I/O, you probably want it to default to IO
, and if it does heavy computation, you probably want it to default to Default
.
Let me give you an example. Let’s say we have a ProgressIndicator
class which performs some UI work for us:
class ProgressIndicator(
private val coroutineScope: CoroutineScope
) {
fun start(
timeout: Milliseconds,
message: String? = null
) = coroutineScope.launch {
...
}
}
Here, start(...)
will use whichever dispatcher is the default in the injected CoroutineScope. This is convenient when it comes to testing, since we can inject a CoroutineScope such as TestCoroutineScope
which uses the TestCoroutineDispatcher
, allowing us to have explicit time control. However, in production code I would like to be certain that the CoroutineScope being injected is always going to default to the Main
dispatcher/thread, or perhaps even Main.immediate
.
Note that there are other ways to ensure code always executes on a given dispatcher. Here, we could be using coroutineScope.launch(Dispatchers.Main) { ... }
for that effect. However, there are other issues with this when it comes to verbosity of code and testing. I will be addressing those concerns in my next article, so for now I’ll ask you to bear with me.
If using dependency injection, you can get kind of close to ensuring the use of Main
by using qualifiers. Qualifiers will ensure that when your DI framework creates new instances, it will have the correct type of CoroutineScope
.
As an example, a Dagger2 implementation may look like this:
@Qualifier
annotation class MainCoroutineScope
@Module
object CoroutineScopeModule {
@Provides
@MainCoroutineScope
fun provideMainCoroutineScope(): CoroutineScope = CoroutineScope(Job() + Dispatchers.Main)
}
class ProgressIndicator @Inject constructor(
@MainCoroutineScope
private val coroutineScope: CoroutineScope
) {
...
}
This is fine for Dagger (or any other DI framework), but DI frameworks don’t have a monopoly on creating instances of classes. Sometimes, we still need to create instances manually — particularly with coroutines because of structured concurrency. And constructors aren’t the only way we pass arguments. What if a function (such as in a builder or a factory) requires a special type of CoroutineScope
?

Markers to the rescue
Instead of relying upon DI to partially do our job for us, it would be nice if we could use the type system. If we can capture our CoroutineScope
requirement in a type, the project won't build unless that requirement is met. We can do this by creating marker interfaces like these:
interface DefaultCoroutineScope : CoroutineScope
interface IOCoroutineScope : CoroutineScope
interface MainCoroutineScope : CoroutineScope
interface MainImmediateCoroutineScope : CoroutineScope
interface UnconfinedCoroutineScope : CoroutineScope
Note thatMainImmediate
refers toDispatchers.Main.immediate
, which is basicallyDispatchers.Main
but with immediate execution.
We can then change our CoroutineScope
dependency accordingly:
class ProgressIndicator(
private val coroutineScope: MainCoroutineScope
) {
...
}

Factories
These markers by themselves don’t accomplish much, but you can pair them with factory functions:
public fun MainCoroutineScope(
job: Job = SupervisorJob()
): MainCoroutineScope = object : MainCoroutineScope {
override val coroutineContext = job + Dispatchers.Main
}
public fun MainCoroutineScope(
context: CoroutineContext
): MainCoroutineScope = object : MainCoroutineScope {
override val coroutineContext = context + Dispatchers.Main
}
// repeat for the other dispatchers
Now, we can easily create implementations of the markers which guarantee to have the proper default dispatcher:
val scope = MainCoroutineScope()
val progressIndicator = ProgressIndicator(scope)
Note that there is a lot of subtlety to defining and creating objects which require aCoroutineScope
, again because of structured concurrency. If we assume thisProgressIndicator
is going to be limited to the lifecycle of some creator screen, then it makes sense to just pass in the sameMainCoroutineScope
of the class which is creating it. However, if thisProgressIndicator
is going to live on beyond its creator, then it should have a newMainCoroutineScope
, and care should be taken to make sure that the scope is closed whenever theProgressIndicator
is meant to be destroyed.
Going back to Dagger2, we can delete the qualifiers entirely and rewrite a simpler CoroutineScopeModule
like so:
@Module
object CoroutineScopeModule {
@Provides
fun provideMainCoroutineScope(): MainCoroutineScope = MainCoroutineScope()
}
You can find complete implementations of the module for Dagger2 and Koin here.

Testing
I’ve hinted a couple of times — I have some very strong opinions about testing with coroutines (and in general…). I won’t hijack my own article and go into all those opinions here, but one of those opinions matters a lot right now.
You should probably be using TestCoroutineScope
and TestCoroutineDispatcher
from kotlinx.coroutines-test
in the majority of your testing.
But that isn’t so simple if you follow the advice I’ve been giving above. TestCoroutineScope
only implements CoroutineScope
, since of course kotlinx.coroutines
has no concept of the interfaces I defined above. This means that if you have a class which requires a MainCoroutineScope
, you can't just give it a TestCoroutineScope
. For this, we'll need one more marker, a class, and a factory:
@ExperimentalCoroutinesApi
interface TestPolymorphicCoroutineScope : TestCoroutineScope,
DefaultCoroutineScope,
IOCoroutineScope,
MainCoroutineScope,
MainImmediateCoroutineScope,
UnconfinedCoroutineScope
@ExperimentalCoroutinesApi
private class TestPolymorphicCoroutineScopeImpl(
context: CoroutineContext = EmptyCoroutineContext
) : TestPolymorphicCoroutineScope,
// delegates all functionality to TestCoroutineScope
// uses the TestCoroutineScope factory function
// to inject the TestCoroutineDispatcher and TestCoroutineExceptionHandler into the context
TestCoroutineScope by TestCoroutineScope(context)
@ExperimentalCoroutinesApi
fun TestPolymorphicCoroutineScope(
context: CoroutineContext = EmptyCoroutineContext
): TestPolymorphicCoroutineScope = TestPolymorphicCoroutineScopeImpl(
context = context + dispatcher
)
This interface serves as a TestCoroutineScope
or any of our marker variants. And since they all implement CoroutineScope
, it does as well.
The class and factory both mirror those created for TestCoroutineScope
, and delegate all actual of their logic to that library -- meaning that any changes made to the kotlinx.coroutines-test
library will be reflected in this code as well.
Now, we can create a JUnit 4 rule to automate the creation and cleanup of this TestPolymorphicCoroutineScope
for us:
class CoroutineTestRule : TestRule,
TestPolymorphicCoroutineScope by TestPolymorphicCoroutineScope() {
val dispatcher = coroutineContext[ContinuationInterceptor] as TestCoroutineDispatcher
override fun apply(
base: Statement, description: Description?
) = object : Statement() {
override fun evaluate() {
@Throws(Throwable::class)
override fun evaluate() {
Dispatchers.setMain(dispatcher)
base.evaluate()
cleanupTestCoroutines()
Dispatchers.resetMain()
}
}
}
}
This Rule is fairly typical for a coroutine test Rule. If you’d like to learn more about CoroutineTestRule
or the JUnit5 version, CoroutineTestExtension
, you can read about them in one of my earlier articles here.
Closing
That’s all for now. If you’d like to learn more about coroutines, be sure to follow me. My next article will be on how to completely abstract away our dependence upon that Dispatchers
object.
If you’d like to check out my library which has already implemented this pattern, as well as a good deal more, please check out Dispatch.