Sargun Vohra
06/11/2025, 8:48 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)Chrimaeon
06/11/2025, 9:02 PMChrimaeon
06/11/2025, 9:03 PMSargun Vohra
06/11/2025, 9:04 PMChrimaeon
06/11/2025, 9:06 PMSargun Vohra
06/11/2025, 9:10 PMLaunchedEffect
calling into a suspend fun
generated by Ktorfit and backed by the OkHttp engine.
The Ktor http client:
HttpClient(OkHttp.create()) {
install(HttpCache) { }
install(ContentNegotiation) { json(PokeApiJson, ContentType.Any) }
expectSuccess = true
}
The list Composable:
@Composable
fun PokemonList(padding: PaddingValues, pokemon: NamedApiResourceList) {
Box(Modifier.consumeWindowInsets(padding).fillMaxSize()) {
LazyColumn(contentPadding = padding) {
items(pokemon.results) { summary ->
var result by remember { mutableStateOf<Result<PokemonSpecies>?>(null) }
LaunchedEffect(Unit) { result = PokeApi.getPokemonSpecies(summary.id) }
result
?.onSuccess { PokemonListItem(it) }
?.onFailure { PokemonListItemError(summary, it.message ?: "Unknown error") }
?: PokemonListItemPlaceholder(summary)
}
}
}
}
Edwin
06/12/2025, 4:30 PMMutableStateFlow<Result<PokemonSpecies>?>
Also, I noticed you are using a lot of !!
. I would suggest you to avoid that at all, then when debugging the crash, you should have a clear idea in what line, the crash is happening.
I see, you are using getOrThrow()
... in case of Throw... are you handling it with a try-catch statement?
Would be good, if you posted the error log here.Sargun Vohra
06/12/2025, 4:34 PMEdwin
06/12/2025, 4:44 PMLaunchedEffect(Unit) {
withContext(IO) {
result = PokeApi.getPokemonSpecies(summary.id)
}
}
Sargun Vohra
06/12/2025, 4:45 PMChrimaeon
06/12/2025, 4:54 PMChrimaeon
06/12/2025, 4:54 PMEdwin
06/12/2025, 4:58 PMwithContext(<http://Dispatchers.IO|Dispatchers.IO>) {...}
but the result... should be assigned inside:
withContext(Dispatchers.Main) {...}
and the second suggestion:
would be refactor it to use a viewModel, then collecting a flow in the composable. That's usually the practice I recommend in every project.Sargun Vohra
06/12/2025, 5:34 PMDid you try the release build?Yes I did; sorry I forgot to follow up on that! The release build works well, just like the profileable build. So the issue is only present on a debug build. Still, it's strange to me the UI would freeze with such a simple network call + display data demo with the overhead of debug build.
would be refactor it to use a viewModel, then collecting a flow in the composable. That's usually the practice I recommend in every project.I understand the common practices for production apps. This is a small demo app for a library, not a production app that's ever going to get more complex than this. The problem happens even when there's only one network call loading a single item; it's just harder to see unless the network call is deliberately slowed down (say, from the developer settings toggle to throttle network speed). The problem I'm trying to solve here is: why is the UI freezing during a network call when on a debug build on Android?
ephemient
06/12/2025, 5:53 PM