When a component is destroyed, its CoroutineScope ...
# coroutines
z
When a component is destroyed, its CoroutineScope is cancelled. Id like to do one last thing at that point, do I do
GlobalScope.launch {}
or `scope.launch(context = NonCancellable) {}`; or are they basically the same thing? My operation is just a few millis long.
s
Is that operation suspending? If it is not, can you try getting the
Job
object out of it and attaching a
invokeOnCompletion
on it? Smth like
Copy code
val myScope: CoroutineScope
myScope.coroutineContext[Job]?.invokeOnCompletion {
  delay()
}
z
Thanks @Stylianos Gakis but unfortunately it's a suspend call!
n
Do you need to wait for the other work to finish before the last thing is done? Does this last thing use any resources owned exclusively by the component? When talking about
CoroutineScope
, we are talking about lifetimes of work and/or resources and the relationship between those lifetimes. So the answer depends on how your lifetimes are relate to each other. Reserve
GlobalScope
for singleton's and resources that last the entire process. If your component has work with one lifetime and some other work with another, then feel free to use multiple `CoroutineScope`s (though it can also be a warning that you should split up your class). I would never use
scope.launch(context = NonCancellable) {}
, calling launch with a
context
that contains a
Job
is confusing. I mean, if you aren't using the scope's job, why use it at all? If
scope
is cancelled, would this even run or no? I probably wouldn't use
GlobalScope.launch {}
directly since if I'm using global resources (the only time I'd use
GlobalScope
) then I'd probably wrap those resources into a singleton and so I'd be calling a non-suspend method that calls
GlobalScope.launch
internally so
invokeOnCompletion
would work. But don't use
GlobalScope
to clean up resources owned by the component, you lose the ability to wait on
scope
to know when those resources are actually freed. If I had work lifetime and a work+cleanup lifetime. I'd might go with something like:
Copy code
val componentScope = CoroutineScope() //or however you define it
//Cancelling componentScope will also cancel workScope due to child/parent Jobs
//Waiting for componentScope, waits for work + cleanup to all finish
val workScope = CoroutineScope(SupervisorJob(componentScope.coroutineContext.job))
init {
    //UNDISPATCHED ensures it starts before cancellation could possibly happen
    componentScope.launch(start = CoroutineStart.UNDISPATCHED) {
        withContext(NonCancellable) { // clearer than passing it into launch
            workScope.join() //wait for all work to finish
            cleanUpSuspending()
        }
    }
}
z
Thank you @Nick Allen! Thats a solution I probably wouldnt have thought of, but now that you mention it (and Ive dug into it a bit to understand how/why it work), it just makes sense and works perfectly for my use-case.
a
Why don't you just do it in a
finally
block? If you only want to do it when the coroutine is cancelled, then:
Copy code
try {
    ...
} catch (e: CancellationException) {
    cleanUp()
    throw e
}
z
@Albert Chang I guess that works too? I do like that its simpler and easier to reason about. Is that guaranteed to always run? Im just thinking about how a long running cleanUp (which isnt the case here, just trying to wrap my head around it) would keep a cancelled scope running, potentially forever?
@Albert Chang Actually, it doesnt work.. CleanUp does get called, but isnt able to finish its work in time.
If I wrap cleanUp in withContext(NonCancellable) it works, which makes sense
n
If you don’t join on the other work, then the other work may still be running when you run your finally(if that's ok, then go ahead and skip the second scope), and if you don't use NonCancellable, then your suspending cleanup will throw a cancellation exception and not finish. It’s not advisable to cleanup with suspending code but if you must, then always use a NonCancellable block.