Skip to content

Utility for creating child scopes #2758

Open
@circusmagnus

Description

@circusmagnus

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 use viewModelScope.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 parents 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 { ... }
}

Metadata

Metadata

Assignees

No one assigned

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions