https://kotlinlang.org logo
Title
u

ursus

03/07/2023, 2:55 PM
Hey,
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

uli

03/07/2023, 3:01 PM
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

ursus

03/07/2023, 3:05 PM
That's surprising ..
You mean that it feels wrong to me, or that it works? 😄
u

uli

03/07/2023, 3:05 PM
It’s surprising that it works
s

streetsofboston

03/07/2023, 3:06 PM
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

ursus

03/07/2023, 3:07 PM
Yea, it never occurred to me to make it work this way, until this code review. I always tried to do something like this
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

streetsofboston

03/07/2023, 3:09 PM
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

ursus

03/07/2023, 3:09 PM
but one benefit of the surprising version is, that you can execute stuff after
thatSomething
"synchronously"
s

streetsofboston

03/07/2023, 3:09 PM
(ie I'd put appScope.launch { ... } inside a method of another object with the appScope lifecycle)
u

ursus

03/07/2023, 3:09 PM
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

streetsofboston

03/07/2023, 3:11 PM
You could expose/return the Job returned by appScope.launch.
u

ursus

03/07/2023, 3:12 PM
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

ursus

03/07/2023, 3:23 PM
interesting .. I also thought it would not work, but testing it on android it does
maybe some sideffect of the android Main dispatcher?
u

uli

03/07/2023, 3:38 PM
I updated my test case and can confirm that it work as you say.
u

ursus

03/07/2023, 3:39 PM
but why, you only moved to a blocking
main
, thats all?
u

uli

03/07/2023, 3:40 PM
i prevented main from exiting
u

ursus

03/07/2023, 3:40 PM
okay so
suspend
main is not a thing?
u

uli

03/07/2023, 3:40 PM
not sure, maybe the final delay made the difference
u

ursus

03/07/2023, 3:41 PM
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

uli

03/07/2023, 4:02 PM
I’ll post that back to the channel…
Hi there, can any one explain, why the
delay(1000)
is not canceled in this code:
fun main() = runBlocking(Dispatchers.Default) {
    val viewModelScope = CoroutineScope(Dispatchers.Default)
    val appScope = CoroutineScope(Dispatchers.Default)
    viewModelScope.launch() {
        println("Coroutine launched in viewModelScope!")
        withContext(appScope.coroutineContext) {
            try {
	            println("Coroutine launched in appScope?")
    	        delay(1000)
        	    println("One second passed")
            } catch (c: CancellationException) {
                println("appScope canceled: c")
				throw c
            }
        }
    }
    delay(100)
	viewModelScope.cancel()
    println("viewModelScope canceled")
    delay(1500)
    println("Done")
}
c

CLOVIS

03/07/2023, 4:08 PM
The job is running in
appScope
, not in
viewModelScope
. You cancelled
viewModelScope
, so it's not impacted.
u

uli

03/07/2023, 4:09 PM
I was expecting it to be running in viewModelScope with the context of the app scope
c

CLOVIS

03/07/2023, 4:12 PM
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

streetsofboston

03/07/2023, 4:15 PM
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

CLOVIS

03/07/2023, 4:15 PM
If you do want to detach the child from the parent, it's much more readable to write
appScope.launch { … }
than
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

ephemient

03/07/2023, 4:16 PM
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

uli

03/07/2023, 4:17 PM
No, it was the OPs intent to not get that block canceled but I was surprised, that it works
s

streetsofboston

03/07/2023, 4:17 PM
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

ursus

03/07/2023, 4:30 PM
okay so the exact reasoning being: ..it is unexpected? I have to say
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

CLOVIS

03/07/2023, 4:33 PM
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

uli

03/07/2023, 4:33 PM
maybe be more explicit and use
withContext(appScope.coroutineContext.job)
u

ursus

03/07/2023, 4:36 PM
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

uli

03/07/2023, 4:37 PM
yes, or make it explicit in withContext that you intend to pass the job
u

ursus

03/07/2023, 4:39 PM
tbh I never used
withContext(
with anything other than a dispatcher
c

CLOVIS

03/07/2023, 4:39 PM
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

ursus

03/07/2023, 4:40 PM
(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

CLOVIS

03/07/2023, 4:40 PM
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

ursus

03/07/2023, 4:41 PM
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

ephemient

03/08/2023, 5:15 AM
NonCancellable
is useful in certain other circumstances: https://kotlinlang.org/docs/cancellation-and-timeouts.html#run-non-cancellable-block