How do you deal with UI blinks when the state upda...
# compose
d
How do you deal with UI blinks when the state updates too fast then it goes back (example showing the loading icon but the server responds too fast and the icon appears just for 100-200 ms) ? Is there any libs to fix this ? Maybe a debounce function would work
a
i always put a small delay before showing any loading. 200 or 250 ms cant remember. most connections or work is fast anyway that people will rarely ever see it
t
I usually do a component based in the ContentLoadingProgressBar:
Copy code
/**
 * ContentLoadingProgressBar implements a ProgressBar that waits a minimum time to be
 * dismissed before showing. Once visible, the progress bar will be visible for
 * a minimum amount of time to avoid "flashes" in the UI when an event could take
 * a largely variable time to complete (from none, to a user perceivable amount).
 */
☝🏽 1
d
I have a ViewModel with “showLoading” flag So now we have three solutions: 1) Wait a little before updating the state, do not update it if the work is already done 2) Update the state, and wait a little to show the loading on the UI so it doesn’t appear as a blink 3) both solutions at the same time
t
I would say the state should be always updated. The UI is one thing and the real state is another. The "blink" issue is a UI level issue that can be solved there. But personally I would expect the state to always reflect the real state we have and not to have any kind of delays. Otherwise you're trying to solve an UI issue in the VM level.
👍🏻 1
1
p
Putting some kind of delay between loading and succes/error state seems like the best way to go to me.
a
it's baked into the component itself for my case. the indicator starts off as invisible when it enters the composition, and after a few ms it reveals the content. dead simple
d
Yes, I come up with something like this:
Copy code
@Composable
fun LoadingIndicator(
    isLoading: Boolean,
    modifier: Modifier = Modifier,
    minDelay: Long = 300,       // wait this long before showing
    minShowTime: Long = 500     // once shown, stay visible at least this long
) {
    var show by remember { mutableStateOf(false) }
    var startTime by remember { mutableStateOf(0L) }

    LaunchedEffect(isLoading) {
        if (isLoading) {
            // Wait before showing to prevent "blink"
            delay(minDelay)
            if (isLoading) { // still loading after delay
                show = true
                startTime = System.currentTimeMillis()
            }
        } else {
            if (show) {
                // Ensure minimum show time
                val elapsed = System.currentTimeMillis() - startTime
                if (elapsed < minShowTime) {
                    delay(minShowTime - elapsed)
                }
                show = false
            }
        }
    }

    if (show) {
        Box(
            modifier = modifier.fillMaxSize(),
            contentAlignment = Alignment.Center
        ) {
            CircularProgressIndicator()
        }
    }
}
Then use it:
Copy code
@Composable
fun MyScreen(viewModel: MyViewModel = hiltViewModel()) {
    val uiState by viewModel.uiState.collectAsState()

    Box(modifier = Modifier.fillMaxSize()) {
        // Your real content
        Text("Data: ${uiState.data}")

        // Debounced loading indicator
        LoadingIndicator(isLoading = uiState.showLoading)
    }
}
🚀 1
p
Personally I think it's better to handle the delay in VM (and update
showLoading
accordingly) so that UI state = UI but this should work too.
a
it's weird how a
LoadingIndicator
can be 'not' loading. It's even simpler to add it to composition if your uisState.showLoading is true, instead of forwarding that to the component
d
@Pavel Habžanský Both are right, But your statement is in direct contradiction with @Tgo1014 https://kotlinlang.slack.com/archives/CJLTWPH7S/p1756110460813249?thread_ts=1756108123.272309&channel=CJLTWPH7S&message_ts=1756110460.813249
@Alex Styl yes the parameter isLoading can be omitted since an LoadingIndicator should always load something
p
@Dumitru Preguza That's fine, I don't think we need to have the same opinion on everything, that's why I said "personally". 🙂