Sargun Vohra
06/11/2025, 9:05 PMsuspend fun
in a LaunchedEffect
, and the client is backed by Ktor (via Ktorfit) and the OkHttp engine. As far as I can tell, these network calls shouldn't be blocking the main thread, but still the UI stutters when I scroll (triggering network calls as list items load).
The problem only happens on Android. The demo on iOS, Desktop (JVM), macOS (native), JS, and WASM builds all have a totally smooth UI.
I've tried:
• Explicitly setting <http://Dispatchers.IO|Dispatchers.IO>
◦ No change. Verified in Ktor source that the default is IO, so that makes sense.
• Switching to the CIO
engine on the JVM target.
◦ No change, still stutters when loading data on Android
• Running as profileable to debug further
◦ The problem disappears when I run a profileable build; scrolling is smooth while data loads, so it's unclear to me how to debug further or gather data on the issue
You can repro by cloning https://github.com/PokeAPI/pokekotlin and running the demo app (the :demo-app
module)
(x-post from #C0B8M7BUY)shikasd
06/12/2025, 1:04 PMshikasd
06/12/2025, 1:05 PMSargun Vohra
06/12/2025, 5:23 PMSargun Vohra
06/12/2025, 5:25 PMshikasd
06/12/2025, 5:27 PMSargun Vohra
06/12/2025, 5:27 PMI've tried:
...
• Running as profileable to debug further
◦ The problem disappears when I run a profileable build; scrolling is smooth while data loads, so it's unclear to me how to debug further or gather data on the issue
shikasd
06/12/2025, 5:28 PMSargun Vohra
06/12/2025, 5:29 PMshikasd
06/12/2025, 5:30 PMSargun Vohra
06/12/2025, 5:30 PMshikasd
06/12/2025, 5:32 PMSargun Vohra
06/12/2025, 7:50 PMshikasd
06/12/2025, 7:55 PMSargun Vohra
06/12/2025, 7:57 PMSargun Vohra
06/12/2025, 7:59 PMSargun Vohra
06/12/2025, 8:22 PM<http://Dispatchers.IO|Dispatchers.IO>
explicitly set in the result converter, the outcome is the same. Code snippet:
override suspend fun convert(result: KtorfitResult): Result<Any> {
return when (result) {
is KtorfitResult.Failure -> Result.failure(result.throwable)
is KtorfitResult.Success -> {
withContext(getDispatcherForDeserialization()) {
when {
result.response.status.isSuccess() ->
Result.success(result.response.body(typeData.typeArgs.first().typeInfo))
// we configure the client with expectSuccess
else -> error("impossible: " + result.response)
}
}
}
}
}
// non-browser platforms
internal actual fun getDispatcherForDeserialization() = Dispatchers.IO
Sargun Vohra
06/12/2025, 8:23 PMwith
instead of withContext
, lolSargun Vohra
06/12/2025, 8:25 PMshikasd
06/12/2025, 8:27 PMwithContext
inside launched effects?Sargun Vohra
06/12/2025, 8:32 PMLaunchedEffect(Unit) {
withContext(getIoDispatcher()) { result = PokeApi.getPokemonSpecies(summary.id) }
}
// androidMain
actual fun getIoDispatcher() = <http://Dispatchers.IO|Dispatchers.IO>
shikasd
06/12/2025, 8:37 PMSargun Vohra
06/12/2025, 8:52 PMAleksei Tirman [JB]
06/16/2025, 8:36 AMLaunchedEffect(Unit) { withContext(Dispatchers.Default) { result = PokeApi.getPokemonSpeciesList(0, 100000) } }
In the debugger, I can see that the thread where the deserialization happens is now DefaultDispatcher-worker-1
.Sargun Vohra
06/16/2025, 9:57 AMAleksei Tirman [JB]
06/16/2025, 10:10 AMColton Idle
06/18/2025, 4:02 AM