Hey, ```class FooViewModel(private val appScope: ...
# coroutines
u
Hey,
Copy code
class FooViewModel(private val appScope: CoroutineScope) : ViewModel() {

    fun fooClicked() {
        viewModelScope.launch {
            delay(2000)
            withContext(appScope.coroutineContext) { <---------------------------
                // Should run even when view user clicks back (i.e. view model cleared)
                delay(2000)
            }
        }
    }
}
Is this a correct way to run something even after view model scope is cancelled? (view model cleared on back press) I was used to just
appScope.launch { .. }
i.e. to launch a new couroutine in the "higher" scope explicitly This way of
withContext
seems to work as well but feels wrong
Opinions? Doesn't it leak
viewmodel this
during the lambda execution?
u
This way of
withContext
seems to work as well but feels wrong
That’s surprising, because structural concurrency is a feature of the CoroutineScope, not of the Context and
withContext
only replaced the Context as far as I know.
Doesn’t it leak
viewmodel this
during the lambda execution?
True, there is a risk, but only if you reference the ViewModel. But how would that work any way, accessing the view model for more than it’s life cycle. Btw. same would be true for the lambda of
launch
u
That's surprising ..
You mean that it feels wrong to me, or that it works? 😄
u
It’s surprising that it works
s
Using the other scope's Job (in the appScope.coroutineContext) like this doesn't seem quite right. I think it's better to 'switch' CoroutineScopes (appScope) to properly communicate between objects with different lifecycles (scopes).
u
Yea, it never occurred to me to make it work this way, until this code review. I always tried to do something like this
Copy code
class FooViewModel(private val something: SomethingInHigherScope) : ViewModel() {

    fun fooClicked() {
        viewModelScope.launch {
            delay(2000)
            sometihng.thatSomething()
        }
    }
}

class SomethingInHigherScope(private val appScope: CoroutineScope) {
    fun thatSomething() {
        appScope.launch {
            // Should run even when view user clicks back (i.e. view model cleared)
            delay(2000)
        }
    }
}
s
Also, I would not expose
appScope
publicly like this or pass it to the method of another object, which can run the risk of the other object accidentally do something destructive to it (eg cancelling it accidentally)
u
but one benefit of the surprising version is, that you can execute stuff after
thatSomething
"synchronously"
s
(ie I'd put appScope.launch { ... } inside a method of another object with the appScope lifecycle)
u
with mine you'd have to expose some flows, or a Job it self to join on
yea scope injecting is another question, I too have a private scope in
SomethingInHigherScope
.. but then you need a explicit destructor
s
You could expose/return the Job returned by appScope.launch.
u
yea, but should you? tbh that also feels wrong 😄 running the action on "my" scope, but then letting callsites control it
best would probably be some sort of Job interface which doesnt allow for cancellation, just joining
anyways, back to the original .. would you consider the
withContext(appScope.coroutineScope)
pattern a antipattern? some sideffect? desired behavior?
u
interesting .. I also thought it would not work, but testing it on android it does
maybe some sideffect of the android Main dispatcher?
u
I updated my test case and can confirm that it work as you say.
u
but why, you only moved to a blocking
main
, thats all?
u
i prevented main from exiting
u
okay so
suspend
main is not a thing?
u
not sure, maybe the final delay made the difference
u
anyways, what now haha 😄
I did see
withContext(NonCancellable)
in the docs, which seem somewhat similar, but that's a
AbstractCoroutineContextElement
so probably doesn't apply
u
I’ll post that back to the channel…
c
The job is running in
appScope
, not in
viewModelScope
. You cancelled
viewModelScope
, so it's not impacted.
u
I was expecting it to be running in viewModelScope with the context of the app scope
c
The thing that decides whether a coroutine is cancelled is the
Job
instance, which is stored in
coroutineContext
. Print
coroutineContext
in all your println statements to see what is running in what. What you give to
withContext
overrides the caller. Because you used
appScope
's context (which contains a
Job
), the contents of
withContext
are children of that job, and not of the caller's. The caller suspends until
withContext
is over.
It's not really considered good practice to call
withContext
with a context that contains a Job, exactly for this reason: it detaches the child coroutine from the caller
s
If you just want to make it non-cancellable, use
NonCancellable
in the withContext call or if you want to execute the async code on a different lifecycle, a different scope, launch it in that coroutine-scope.
c
If you do want to detach the child from the parent, it's much more readable to write
Copy code
appScope.launch { … }
than
Copy code
withContext(appScope) { … }
but it essentially is the same thing. (the difference is: with the latter option, the coroutine will inherit all context elements that are not present in
appScope
from its parent, e.g. a
CoroutineName
)
e
are you trying to launch something that can be cancelled by both
viewModelScope
and
appScope
? a Job can only have one parent (or zero)
u
No, it was the OPs intent to not get that block canceled but I was surprised, that it works
s
I did some experimentations my self a long while ago with switching scopes 🙂 (towards the bottom of this article0: https://medium.com/swlh/how-can-we-use-coroutinescopes-in-kotlin-2210695f0e89
u
okay so the exact reasoning being: ..it is unexpected? I have to say
Copy code
class EmailSender {

    suspend fun sendEmail() {
        ...
    }
}

class NewEmailViewModel(private val sender: EmailSender, private val appScope: CoroutineScope) {

    fun sendClicked() {
        viewModelScope.launch {
            delay(1000)
            val job = appScope.launch {
                sender.sendEmail()
            }
            job.join()
            somethingOther()
    }

    fun sendClicked2() {
        viewModelScope.launch {
            delay(1000)
            withContext(appScope.coroutineContext) {
                sender.sendEmail()
            }
            somethingOther()
        }
    }
}
withContext
seems like bit more natural api
c
It really depends on what you want to do. Indeed if you want to launch a job in another scope and only wait for it (but not cancel it if you're cancelled), it's clearer to use
withContext
.
u
maybe be more explicit and use
Copy code
withContext(appScope.coroutineContext.job)
u
so.. you'd more precisely say, it is unexpected that stuff launched into a scope, could outlive the given scope (i..e structured concurrency) and if you want to this, you should be explicit about it? (explicit =
otherScope.launch
)
u
yes, or make it explicit in withContext that you intend to pass the job
u
tbh I never used
withContext(
with anything other than a dispatcher
c
Yes, most code (but there are sometimes good reasons to deviate from the standard) is to write code that either: • waits for a task to finish, and cancels the task if it is cancelled • fires-and-forget a task in another scope, and doesn't wait for its results In your example, why do you need to wait for the email to be sent?
I use
withContext
often with stuff like
CoroutineName
, or my own custom context elements, but I tend to avoid using it with a
Job
u
(no reason to wait for the email, just to illustrate it is easy to do so vs. having a Job join/Flow<EmailSentEvent> boilerplate)
c
I think I recall someone from the coroutine team saying they would have liked to forbid
withContext
with a job, but it's too late now because it would break code [citation needed]
u
But then
NonCancellable
could not exist, no? Which to me always felt like some sort of legacy solution to make some old blocking stuff work
e
NonCancellable
is useful in certain other circumstances: https://kotlinlang.org/docs/cancellation-and-timeouts.html#run-non-cancellable-block