svenjacobs
08/21/2025, 6:11 AMsvenjacobs
08/21/2025, 6:11 AMfun onButtonClick() {
mainScope.launch {
performNetworkRequestUseCase()
}
}
mainScope
is a singleton (injected) instance of MainScope
and performNetworkRequestUseCase
is a class that looks like this
suspend fun invoke() {
withContext(Dispatchers.IO) {
doNetworkRequest()
}
}
The default dispatcher of MainScope
is Main
so obviously anything that runs in there might interfere with UI rendering. Since I'm aware of that I used withContext(<http://Dispatchers.IO|Dispatchers.IO>)
in invoke
of the use case.
However, the stuttering only disappeared after I changed the code in the ViewModel to the following
fun onButtonClick() {
mainScope.launch(Dispatchers.IO) { // note the Dispatchers.IO here
performNetworkRequestUseCase()
}
}
Now I'm confused. Why do I have to explicitly declare a dispatcher for launch
when the suspending functions does the same with withContext
? Why does it make a difference? There is just this single call of the use case. There's no other operation in the coroutine.svenjacobs
08/21/2025, 6:13 AMMainScope
in the ViewModel and not the provided viewModelScope
because this operation should keep running even when the ViewModel was disposed.svenjacobs
08/21/2025, 6:42 AMmainScope.launch {
withContext(<http://Dispatchers.IO|Dispatchers.IO>) {
doNetworkRequest()
}
}
is different to
mainScope.launch(<http://Dispatchers.IO|Dispatchers.IO>) {
withContext(<http://Dispatchers.IO|Dispatchers.IO>) {
doNetworkRequest()
}
}
Does that make sense? ๐ตโ๐ซLukasz Kalnik
08/21/2025, 8:20 AM<http://Dispatchers.IO|Dispatchers.IO>
at all when calling the network request. The libraries do it for you under the hood.
2. If you need to have a request which survives ViewModel being canceled/cleared, then better solution is to use something like this
object ApplicationScope {
val instance = CoroutineScope(SupervisorJob())
}
Also I'm not sure how correct it is to launch a request, which needs to survive the ViewModel, on injected mainScope
inside a ViewModel. Wouldn't it hold a reference to the mainScope
instance of the destroyed ViewModel? Probably that's not an issue though, only the ViewModel cannot be garbage collected, but this should not be your problem here.
We do the requests which need to survive the ViewModel being destroyed inside a Repository. Repository lives the whole application lifecycle.svenjacobs
08/21/2025, 8:26 AMobject ApplicationScope {
val instance = CoroutineScope(SupervisorJob())
}
is any different to how I do it already. mainScope
is a singleton managed via dependency injection. MainScope
is a Coroutine with a SupervisorJob
so it's basically the same? Anyway, I don't think the issue is the injected mainScope
.
The network call is a Firebase Functions call
functions.getHttpsCallable("function").call().await()
where await
is a suspend
extension function on Task<T>
, which is returned by getHttpsCallable
.Lukasz Kalnik
08/21/2025, 8:27 AM<http://Dispatchers.IO|Dispatchers.IO>
?Lukasz Kalnik
08/21/2025, 8:27 AMsvenjacobs
08/21/2025, 8:27 AMLukasz Kalnik
08/21/2025, 8:29 AMMainScope
is different. It has Dispatchers.Main
. My ApplicationScope
doesn't set a dispatcher.Lukasz Kalnik
08/21/2025, 8:30 AM<http://Dispatchers.IO|Dispatchers.IO>
correctly for itself.Lukasz Kalnik
08/21/2025, 8:31 AM<http://Dispatchers.IO|Dispatchers.IO>
is only needed for blocking functions.svenjacobs
08/21/2025, 8:31 AMlaunch(<http://Dispatchers.IO|Dispatchers.IO>)
is required when there is already a withContext(<http://Dispatchers.IO|Dispatchers.IO>)
? Regardless of what Dispatcher the CoroutineScope has set, the withContext
should "override" that, no?svenjacobs
08/21/2025, 8:34 AMawait
from the kotlinx-coroutines-play-services
artifact (and awaitImpl
) does not set a Dispatcher.svenjacobs
08/21/2025, 8:36 AMTask
is from the Play Services SDK and does not necessarily have to be a network request.Lukasz Kalnik
08/21/2025, 8:37 AMawaitImpl
has to set a Dispatcher at all, because it's not blocking. It's just suspending a coroutine, that's different.Lukasz Kalnik
08/21/2025, 8:37 AMLukasz Kalnik
08/21/2025, 8:40 AMLukasz Kalnik
08/21/2025, 8:40 AMLukasz Kalnik
08/21/2025, 8:41 AMMainScope
sets a Dispatchers.Main
firstLukasz Kalnik
08/21/2025, 8:41 AMCalls to withContext whose context argument provides a CoroutineDispatcher that is different from the current one, by necessity, perform additional dispatches: the block can not be executed immediately and needs to be dispatched for execution on the passed CoroutineDispatcher, and then when the block completes, the execution has to shift back to the original dispatcher.
Lukasz Kalnik
08/21/2025, 8:42 AMDispatchers.Main
to <http://Dispatchers.IO|Dispatchers.IO>
svenjacobs
08/21/2025, 8:42 AMwithContext
apparently isn't working.svenjacobs
08/21/2025, 8:42 AMSo you have an (unnecessary) additional dispatch if you switch fromMight be true but I still want to know how and why this impacts thetoDispatchers.Main
<http://Dispatchers.IO|Dispatchers.IO>
Main
dispatcher.Lukasz Kalnik
08/21/2025, 8:42 AMsvenjacobs
08/21/2025, 8:43 AMLukasz Kalnik
08/21/2025, 8:43 AMLukasz Kalnik
08/21/2025, 8:44 AMLukasz Kalnik
08/21/2025, 8:44 AMMichael Paus
08/21/2025, 9:09 AMstreetsofboston
08/21/2025, 9:19 AMsvenjacobs
08/21/2025, 9:20 AMMichael Paus
08/21/2025, 9:27 AMobject ApplicationScope
any different from using GlobalScope
.Lukasz Kalnik
08/21/2025, 9:55 AMsvenjacobs
08/21/2025, 1:32 PMMainScope()
to CoroutineScope(SupervisorJob())
for our global application scope did actually "solve" the problem of having to specify a dispatcher for launch
, but I would still like to know why withContext
alone doesn't seem to work or rather has an impact on the main (UI) thread?Lukasz Kalnik
08/21/2025, 1:39 PMMain
dispatcher, and only then withContext
dispatches it on another thread. I don't know how big the impact is though and how much stuttering you experienced.CLOVIS
08/21/2025, 1:51 PMwithContext
call and leave the main thread available for whatever else? I don't understand how this would cause stuttering.svenjacobs
08/21/2025, 1:54 PMLukasz Kalnik
08/21/2025, 1:54 PMsvenjacobs
08/21/2025, 1:55 PMLukasz Kalnik
08/21/2025, 1:57 PMstreetsofboston
08/21/2025, 2:13 PMZach Klippenstein (he/him) [MOD]
08/21/2025, 9:24 PMZach Klippenstein (he/him) [MOD]
08/21/2025, 9:25 PMwithContext
case the network call is happening on the main thread, but i'd want to add some logging or debug or profile to actually see what's causing the UI stutter before jumping to assumptions.Zach Klippenstein (he/him) [MOD]
08/21/2025, 9:25 PMsvenjacobs
08/22/2025, 7:20 AMstreetsofboston
08/22/2025, 7:22 AMsvenjacobs
08/22/2025, 7:26 AMsvenjacobs
08/22/2025, 7:29 AMstreetsofboston
08/22/2025, 7:43 AMRoman Golyshev
08/22/2025, 10:51 AMcoroutineContext
for both cases in your code?
Just doing println(coroutineContext)
inside of launch(<http://Dispatchers.IO|Dispatchers.IO>) { ... }
vs launch { withContext(<http://Dispatchers.IO|Dispatchers.IO>) { โฆ } }
would be enough
I just want to make sure that the correct CoroutineDispatcher
is indeed present in the contextsvenjacobs
08/22/2025, 12:20 PMlaunch
without any dispatcher where First context
is right after launch
and Second context
is inside the withContext(<http://Dispatchers.IO|Dispatchers.IO>)
First context [StandaloneCoroutine{Active}@5f2d8ef, Dispatchers.Main]
Second context [DispatchedCoroutine{Active}@e595ffc, <http://Dispatchers.IO]|Dispatchers.IO]>
and this is with launch(<http://Dispatchers.IO|Dispatchers.IO>)
First context [StandaloneCoroutine{Active}@f2a1881, <http://Dispatchers.IO]|Dispatchers.IO]>
Second context [kotlinx.coroutines.UndispatchedMarker@d29ed26, UndispatchedCoroutine{Active}@27fbd67, <http://Dispatchers.IO]|Dispatchers.IO]>
svenjacobs
08/22/2025, 12:45 PMstreetsofboston
08/22/2025, 12:47 PMLukasz Kalnik
08/22/2025, 12:56 PMsvenjacobs
08/22/2025, 12:59 PM