I'm curious about why `CoroutineScope(…)` requires...
# coroutines
s
I'm curious about why
CoroutineScope(…)
requires a
coroutineContext
argument. By default it creates its own
Job()
. It will also use
Dispatchers.Default
if I don't specify a dispatcher. So what other coroutine context would I actually want to pass? It feels strange to have to write
CoroutineScope(EmptyCoroutineContext)
, not to mention misleading since the resulting scope will not have an empty coroutine context. And it's inconsistent with
MainScope()
, which takes no arguments at all. 🤔
It's also kind of weird that
CoroutineScope(…)
defaults to make a
Job()
while
MainScope()
makes a
SupervisorJob()
. I'm not sure why those are different.
r
Generally, an inner coroutine scope inherits the context of its parent. Therefore, it is sensible that the context has to be passed donw upon its creation. A coroutineContext, wrapped by a scope, is an IndexedSet of different elements that compose it. Those can be the dispatcher, as you mentioned, but also the coroutine's name, other implementation details, or even context injected at the parent's level by your own implementation. ---
s
Right, but if I was making a child of an existing scope, I'd use
coroutineScope { … }
or just
launch { … }
. As I understand it,
CoroutineScope(…)
is primarily intended for top-level scopes, so there'd be nothing to inherit. I take your point about the coroutine name and other context options. With
MainScope()
we're encouraged to pass those using
MainScope() + context
. So there's still a disparity.
r
now, the separation between coroutineScope using a Job: The default behavior of a coroutine is to cancel when one of its children are cancelled. This ensures that work is not left hanging by default. However, generally we don't want that behavior at top level of an application or framework: For that, you have a SupervisorScope / SupervisorJob, in which this behavior is different: A parent coroutine is not cancelled when one of its children are cancelled
👍 1
s
Really I'm just curious about why the two functions are different. I don't think one approach is necessarily better than the other.
r
Ah, that's the thing, you have the choice to use either CoroutineScope or SupervisorScope depending on your implementation: If I am operating at the top level of an application, I might choose supervisor to not cancel the full app upon cancellation. On a more ephemeral level, I may just keep the cancellation Behavior of CoroutineScope. Now, the point of accepting context is to ensure that elements passed down to your code (say, when you are writing a library or similar) are not erased by ignoring the parent's context
s
Thanks, I appreciate your perspective and you explained well why customising the scope's context can be useful. I wonder if you (or anyone else) have any thoughts on why the starting context for
MainScope()
should be different from that of `CoroutineScope(…)`—besides the dispatcher, I mean—or would be less likely to need customization? Did I understand you right that you think
MainScope()
is more likely to be used solely as a "top-level" scope, while
CoroutineScope(…)
could be appropriate for a wider range of situations? That doesn't quite tally with my own understanding—I see them both as top-level scope builders—but I'm certainly interested in other points of view.
✌🏽 1
c
Another thing, sometimes you have access to a coroutine context, but not its scope, so you can't launch stuff into it. Using
CoroutineScope()
you can convert it back into a scope.
s
MainScope() tends to get used in UIs. Failing (child) tasks in this scope should usually not cancel current and prevent the future launch of other sub-tasks. That could make the UI unresponsive after the first failure. Hence it uses a SupervisorJob instead of a plain Job. (this is my understanding.... I could be very wrong 😁)
👍 1
j
sometimes you have access to a coroutine context, but not its scope, so you can't launch stuff into it. Using CoroutineScope() you can convert it back into a scope.
Roman discourages wrapping a
suspend
function's
CoroutineContext
in a new
CoroutineScope
instance, but to make the function's behavior clear, rather:
If you need to launch a coroutine that keeps running after your function returns, then make your function an extension of
CoroutineScope
or pass
scope: CoroutineScope
as parameter to make your intent clear in your function signature. Do not make these functions suspending.
👍 2
b
> As I understand it,
CoroutineScope(…)
is primarily intended for top-level scopes, so there'd be nothing to inherit. Sometimes you have to use CoroutineScope, but still create parent-child relation:
Copy code
val parentScope = CoroutineScope(....) // initialized somewhere

val componentScope = CoroutineScope(Job(parentScope.coroutineContext.job))
👍 2