Description
I have noticed in my immediate surroundings, that quite a few developers are struggling with scoping their coroutines in easy way. Exemplary problem:
- We have an android ViewModel class. It has a built-in
viewModelScope
, which people commonly use to launch coroutines. It gets automatically canceled, when an android screen goes away. Super useful. - A user searches for some items in an online shop. Our ViewModel collects a few Flows concurrently, with a given search query as an input.
- There are some other concurrent coroutines in action (say - loading an image or updating user gps location)
- A user cancels its search by tapping an
X
. All search-related coroutines should stop. But other coroutines scoped to this viewmodel should continue, so we cannot useviewModelScope.coroutineContext.cancelChildren()
- Alternatively, user goes away from our screen. We need to cancel all corotuines - search-related and others. So our
searchScope
should not be a completely standalone scope, that needs to be canceled manually. It should go down along viewmodelScope automatically
Usual solutions usually include some collection of jobs, like:
class ViewModel {
val searchJobs = emptyList<Job>()
...
fun search() {
searchJobs += viewModelScope.launch { ... }
searchJobs += viewModelScope.launch { ... }
}
fun searchCanceled() {
searchJobs.forEach { cancel }
}
That is a bit boilerplaty and error-prone. If one forgets to add some well-camouflaged job to an appriopiate collection, it will not be canceled. Same problem, that RxJava has with its syntax not forcing a developer to properly scope his async operation.
Coroutines offer an IMHO more neat solution with an CoroutineScope
construct:
class ViewModel {
val searchScope = viewModelScope.newChildScope()
...
fun search() {
searchScope.launch { ... }
searchScope.launch { ... }
}
fun searchCanceled() {
searchScope.cancelChildren()
}
// searchScope gets automatically canceled, when viewModelScope is canceled and inherits all coroutineContext elements from it.
Thing is - there is no utility, that I know of, that would facilitate construction of such childScopes. It needs to be written along this lines:
val childScope = parentScope + Job(parent = parentScope.coroutineContext[Job])
or even
val childScope = CoroutineScope(parentScope.coroutineContext + Job(parent = parentScope.coroutineContext[Job])
It is especially easy to miss attaching of childs scope Job to parent
s Job.
Thus I would propose to add one or two utility functions, that could direct people to (I hope) better solution:
fun CoroutineScope.newChildScope(): CoroutineScope = this + Job(parent = coroutineContext[Job])
fun CoroutineScope.cancelChildren() = coroutineContext.cancelChildren()
The second one I find personally using quite a bit, even if it is just simple shortcut (Just as coroutineContext.cancelChildren()
is just a shortcut to coroutineContext[Job].cancelChildren()
)
newChildScope()
function could optionally have a flag, whether a new job should be a Supervisor or an ordinary one.
Other usecases:
- Launching a job only when fragment / activity is between certain Lifecycle stages:
fun Lifecycle.repeatWhenStarted(job: suspend () -> Unit): Job {
val innerScope: CoroutineScope = coroutineScope.newChildScope()
val innerJob = requireNotNull(innerScope.coroutineContext[Job])
val observer = LifecycleEventObserver { _, event ->
when (event) {
Lifecycle.Event.ON_START -> innerScope.launch { job() }
Lifecycle.Event.ON_STOP -> innerScope.cancelChildren()
else -> Unit
}
}
innerJob.invokeOnCompletion { removeObserver(observer) }
addObserver(observer)
return innerJob
}
(Android X is going to introduce similar function in next release)
- Launching a self-supervising Actor:
class SelfRestartingActor(scope: CoroutineScope) {
val innerScope: CoroutineScope = scope.newChildScope(superVisor = true) + CoroutineExceptionHandler { _, _ ->
startActor()
}
fun startActor() = innerScope.launch { ... }
}