ursus
03/07/2023, 2:55 PMclass 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?uli
03/07/2023, 3:01 PMThis way ofThat’s surprising, because structural concurrency is a feature of the CoroutineScope, not of the Context andseems to work as well but feels wrongwithContext
withContext
only replaced the Context as far as I know.
Doesn’t it leakTrue, 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 ofduring the lambda execution?viewmodel this
launch
ursus
03/07/2023, 3:05 PMThat's surprising ..You mean that it feels wrong to me, or that it works? 😄
uli
03/07/2023, 3:05 PMstreetsofboston
03/07/2023, 3:06 PMursus
03/07/2023, 3:07 PMclass 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)
}
}
}
streetsofboston
03/07/2023, 3:09 PMappScope
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)ursus
03/07/2023, 3:09 PMthatSomething
"synchronously"streetsofboston
03/07/2023, 3:09 PMursus
03/07/2023, 3:09 PMSomethingInHigherScope
.. but then you need a explicit destructorstreetsofboston
03/07/2023, 3:11 PMursus
03/07/2023, 3:12 PMwithContext(appScope.coroutineScope)
pattern a antipattern? some sideffect? desired behavior?uli
03/07/2023, 3:22 PMursus
03/07/2023, 3:23 PMuli
03/07/2023, 3:38 PMursus
03/07/2023, 3:39 PMmain
, thats all?uli
03/07/2023, 3:40 PMursus
03/07/2023, 3:40 PMsuspend
main is not a thing?uli
03/07/2023, 3:40 PMursus
03/07/2023, 3:41 PMwithContext(NonCancellable)
in the docs, which seem somewhat similar, but that's a AbstractCoroutineContextElement
so probably doesn't applyuli
03/07/2023, 4:02 PMdelay(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")
}
CLOVIS
03/07/2023, 4:08 PMappScope
, not in viewModelScope
. You cancelled viewModelScope
, so it's not impacted.uli
03/07/2023, 4:09 PMCLOVIS
03/07/2023, 4:12 PMJob
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.withContext
with a context that contains a Job, exactly for this reason: it detaches the child coroutine from the callerstreetsofboston
03/07/2023, 4:15 PMNonCancellable
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.CLOVIS
03/07/2023, 4:15 PMappScope.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
)ephemient
03/07/2023, 4:16 PMviewModelScope
and appScope
? a Job can only have one parent (or zero)uli
03/07/2023, 4:17 PMstreetsofboston
03/07/2023, 4:17 PMursus
03/07/2023, 4:30 PMclass 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 apiCLOVIS
03/07/2023, 4:33 PMwithContext
.uli
03/07/2023, 4:33 PMwithContext(appScope.coroutineContext.job)
ursus
03/07/2023, 4:36 PMotherScope.launch
)uli
03/07/2023, 4:37 PMursus
03/07/2023, 4:39 PMwithContext(
with anything other than a dispatcherCLOVIS
03/07/2023, 4:39 PMwithContext
often with stuff like CoroutineName
, or my own custom context elements, but I tend to avoid using it with a Job
ursus
03/07/2023, 4:40 PMCLOVIS
03/07/2023, 4:40 PMwithContext
with a job, but it's too late now because it would break code [citation needed]ursus
03/07/2023, 4:41 PMNonCancellable
could not exist, no? Which to me always felt like some sort of legacy solution to make some old blocking stuff workephemient
03/08/2023, 5:15 AMNonCancellable
is useful in certain other circumstances: https://kotlinlang.org/docs/cancellation-and-timeouts.html#run-non-cancellable-block