Stronger Typing for CoroutineScopes

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 Maindispatcher/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?


Photo by Camille San Vicente on Unsplash

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 CoroutineScoperequirement 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 that MainImmediate refers to Dispatchers.Main.immediate, which is basically Dispatchers.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 a CoroutineScope, again because of structured concurrency. If we assume this ProgressIndicator is going to be limited to the lifecycle of some creator screen, then it makes sense to just pass in the same MainCoroutineScope of the class which is creating it. However, if this ProgressIndicator is going to live on beyond its creator, then it should have a new MainCoroutineScope, and care should be taken to make sure that the scope is closed whenever the ProgressIndicator 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.


Photo by Science in HD on Unsplash

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.