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 ViewModel
s, 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 ViewModel
is handling presentation logic or state, right? That is its single responsibility. But now we’ve made it also uphold the contract of the CoroutineScope
interface, 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. :)