I’m having trouble wrapping my head around a parag...
# coroutines
p
I’m having trouble wrapping my head around a paragraph in the Coroutines Basics doc:
But what if the extracted function contains a coroutine builder which is invoked on the current scope? In this case, the
suspend
modifier on the extracted function is not enough. Making
doWorld
an extension method on
CoroutineScope
is one of the solutions, but it may not always be applicable as it does not make the API clearer. The idiomatic solution is to have either an explicit
CoroutineScope
as a field in a class containing the target function or an implicit one when the outer class implements
CoroutineScope
. As a last resort, CoroutineScope(coroutineContext) can be used, but such an approach is structurally unsafe because you no longer have control on the scope of execution of this method. Only private APIs can use this builder.
Particularly, what does it mean that having an explicit CoroutineScope as a field on a class containing the target function is preferable to calling CoroutineScope(…)? How else would you create an explicit CoroutineScope as a field, other than
val scope = CoroutineScope(…)
?
What I really want to understand is: it seems like most of the docs push you away from using the GlobalScope to launch coroutines unless you absolutely have to (which makes sense). What there isn’t a lot of is examples of what else you’re supposed to do if you want to launch asynchronous work from a synchronous context, without blocking. (most of the docs rely heavily on runBlocking {} ). The only other clear option I see is explicitly creating and managing a CoroutineScope(), but that paragraph seems to discourage that too.
Hmm… upon further thought, is what that last sentence is actually saying that calling CoroutineScope(…).launch() is a bad idea? (as opposed to instantiating CoroutineScope as a member variable on a class with a defined lifecycle, then calling this.scope.launch() )
because that would make sense.
z
upon further thought, is what that last sentence is actually saying that calling CoroutineScope(…).launch() is a bad idea?  (as opposed to instantiating CoroutineScope as a member variable on a class with a defined lifecycle, then calling this.scope.launch() )
Yea, i think you got it. Docs could definitely be more clear about that
p
thanks, @Zach Klippenstein (he/him) [MOD]!
One further question: when using
Copy code
coroutineScope { ... }
When does that scope get cleaned up/joined? When control leaves the block?
and what does that mean for any callbacks defined within the block, which reference the scope? Is the scope now invalid by the time those callbacks run?
z
the control is actually the other way around – the function can’t return until all the child coroutines started inside the block have completed
so if you do something like
Copy code
coroutineScope {
  launch {
    suspendCoroutine {}
  }
}
The
coroutineScope
function will suspend forever
p
sure, but what about
Copy code
coroutineScope {
    fun onSomeEvent(<params>) {
        launch { work }
    }
    someExternalSynchronousApiThatTakesACallback(onSomeEvent)
}
that launch hasn’t been called by the time control leaves the coroutineScope
I realize this is a weird and contrived example. it’s in the context of something bigger.
z
No i can definitely see code like that ending up in prod. So that’s effectively the same as
Copy code
val job = Job().complete()
CoroutineScope(job).launch {}
p
okay. That’s real good to know. 😆
z
i.e. the launched coroutine will never actually run, because the parent job is already completed
p
so in this case it really would make sense to explicitly create a CoroutineScope() and manage it at the object level.
z
i would think so yep
p
THANK you. That provides a lot of really useful clarity
I really appreciate your time!
haha, since the object in question is a singleton that lasts the length of the program…. it’s really tempting to just use GlobalScope.
z
Some reasons why that’s still a bad idea: 1. Using any hard-coded scope makes your code harder to test. Always prefer injecting a scope, so you have full control over all your object’s launched coroutines in unit tests. 2. When your codebase scales and you end up refactoring your component so that it no longer lasts the lifetime of the program, you already have the right hooks to scope it correctly. 3. Creating an explicit scope makes your intention very clear about which dispatcher you want to use.
CoroutineScope(Dispatchers.Default)
is much more clear than
GlobalScope
imo. It’s not clear that the author necessarily intended
Dispatchers.Default
to be used when using
GlobalScope
, or if they were even aware that’s what it would mean. 4. If you’re writing an android app, you might eventually find it useful to be able to explicitly cancel everything that’s considered “scoped to the lifetime of the app” in UI tests.
p
all but 4 apply here, and all of these are excellent reasons.
thanks, and I will now stop bothering you 😆
z
no worries!
i feel very strongly about
GlobalScope
, happy to rant about it any day 😛
p
My (waaaaaay less informed) instincts align well with that 😆 I’m just trying to get a better handle on what best practices are.
There’s a lot of “please don’t do this” but I’ve been having trouble getting my head around the “please do this instead”
and that’s a lot clearer after this conversation!
and righto 😆
z
glad i could help!