Why your class probably shouldn’t implement CoroutineScope

Prior to the introduction of CoroutineScope and Structured Concurrency, when we wanted to create a coroutine in a class we would do something like this:

class SomeClass {
  fun fireAndForget() {
    launch {
      ...
    }
  }
}

When the coroutines team introduced Structured Concurrency, they changed the coroutine builder functions launch() and async() from being top-level to being extensions with a CoroutineScope receiver.

// old implementationfun 
launch(...): Job {...}

// new implementationfun 
CoroutineScope.launch(...): Job {...}

So, now we need a CoroutineScope in order to create a coroutine. One way to accomplish this is to make our custom classes implement CoroutineScope. We’ve seen this pattern implemented over and over again in sample and production code. Here it is:

class SomeClass : CoroutineScope {
  private val job = Job() // or SupervisorJob()
  override val coroutineContext = job + Dispatchers.Main
 
  fun fireAndForget() {
    launch {
      ...
    }
  }
}

This works well in classes with a lifecycle, such as the Android ViewModel:

class SomeViewModel : ViewModel(), CoroutineScope {
  private val job = Job()
  override val coroutineContext = job + Dispatchers.Main
  override fun onCleared() {
    cancel() // automatically cancel all coroutines
  }
}

And if our project has a lot of ViewModels, this logic can be abstracted away into an abstract base class to minimize boilerplate:

class SomeViewModel : BaseViewModel()
class SomeOtherViewModel : BaseViewModel()

abstract class BaseViewModel : ViewModel(), CoroutineScope {
  private val job = Job()
  override val coroutineContext = job + Dispatchers.Main
  override fun onCleared() {
    cancel()
  }
}

But…


The problem

Say we’re creating a ViewModel for use in a screen. The role of a ViewModelis handling presentation logic or state, right? That is its single responsibility. But now we’ve made it also uphold the contract of the CoroutineScopeinterface, opening up an entirely different world of functions.

class SomeScreen {

  lateinit var viewModel: SomeViewModel

  fun onCreate() {
    viewModel.launch { }
    viewModel.async { }
    viewModel.produce<Int> { }
    viewModel.ensureActive()
    viewModel.newCoroutineContext(...)
    viewModel.actor<Int> { }
    viewModel.broadcast<Int> { }
    viewModel.cancel()
    // there's more
  }

}

These functions have nothing to do with our presentation logic. No screen needs access to those functions, and we don’t want any other class utilizing our ViewModel as a CoroutineScope.

CoroutineScope is nothing but an implementation detail, and should not be exposed to the consumers of the class.

The solution

Instead of implementing CoroutineScope directly in our class, it should just be a normal property of the class.

abstract class BaseViewModel : ViewModel() {
  /*
  * this could be constructor injected, 
  * field injected, or service-located,
  * but that's a story for a different day
  */
  protected val scope = CoroutineScope(
    Job() + Dispatchers.Main
  )
  override fun onCleared() {
    scope.cancel()
  }
}

With the CoroutineScope as a property, we can now create new coroutines like so:

class SomeViewModel : BaseViewModel() {
  fun fireAndForget() = scope.launch {
    ...
  }
}

Closing thoughts

I’ve picked on ViewModel just because it’s common and therefore convenient, but there are plenty of use-cases where CoroutineScope may be inappropriately implemented. Unless a class is truly a new implementation of CoroutineScope and intended to be used as such, it probably shouldn’t be implementing the interface.

As I hinted at in a code sample, there are plenty of different approaches for acquiring that CoroutineScope instance. Utilizing a service locator or dependency injection can be extremely valuable when writing tests for your coroutines (or just about anything else). I will cover different approaches for exactly that in my next post, so if you’re interested in reading my thoughts, please click that follow button and stay tuned. :)