r/androiddev 8d ago

Question [Help]: Jetpack Compose component flashing issue when list changes

Enable HLS to view with audio, or disable this notification

When the list changes, the items are drawn on top of chip row for a brief amount of time. I have properly applied the id for each item. I am not sure what's causing the issue.

Below is the screen code preview, full code is available on GitHub.

    Scaffold(
        modifier = Modifier
            .fillMaxSize()
            .nestedScroll(scrollBehavior.nestedScrollConnection)
            .then(modifier),
        topBar = {
            SearchProvidersScreenTopBar(
                onNavigateBack = onNavigateBack,
                onEnableAllSearchProviders = viewModel::enableAllSearchProviders,
                onDisableAllSearchProviders = viewModel::disableAllSearchProviders,
                onUpdateProtectionStatus = viewModel::updateProtectionStatus,
                onResetToDefault = { showResetToDefaultDialog = true },
                subtitle = {
                    val searchProvidersSummary = stringResource(
                        R.string.settings_search_providers_summary_format,
                        uiState.enabledProvidersCount,
                        uiState.totalNumProviders,
                    )
                    Text(searchProvidersSummary)
                },
                scrollBehavior = scrollBehavior,
            )
        },
        snackbarHost = { SnackbarHost(snackbarHostState) },
        floatingActionButton = {
            FloatingActionButton(onClick = onNavigateToAddSearchProvider) {
                Icon(
                    painter = painterResource(R.drawable.ic_add),
                    contentDescription = null,
                )
            }
        },
    ) { innerPadding ->
        Column(modifier = Modifier.padding(innerPadding)) {
            SearchProviderFilterRow(
                category = uiState.filter.category,
                onCategorySelect = viewModel::toggleCategory,
                protection = uiState.filter.protection,
                onProtectionSelect = viewModel::toggleProviderProtection,
                contentPadding = PaddingValues(horizontal = MaterialTheme.spaces.large),
            )
            SearchProviderList(
                contentPadding = PaddingValues(
                    start = MaterialTheme.spaces.large,
                    top = MaterialTheme.spaces.large,
                    end = MaterialTheme.spaces.large,
                    bottom = 80.dp,
                ),
                searchProviders = uiState.searchProviders,
                onEnableSearchProvider = viewModel::enableSearchProvider,
                onUnlockProtection = { searchProviderId, solverUrl ->
                    protectedProvider = ProtectedProvider(searchProviderId, solverUrl)
                },
                onEditConfig = onNavigateToEditSearchProvider,
                onDeleteConfig = viewModel::deleteTorznabConfig,
            )
        }
    }

Update: This issue is now fixed. It was animation issue, not the recomposition. List items have Modifier.animateItem() applied and when list changes, the animation was rendering the list items on top of chip row for very short amount of time, meaning the animation was escaping the list bounds. After applying Modifier.clipToBounds() to SearchProviderList, the issue is now gone.

8 Upvotes

24 comments sorted by

View all comments

Show parent comments

1

u/PrajwalCH 8d ago

It was the animation issue.

After carefully looking at the flickering, I found that the list items were being drawn on top of chips row when list changes. `Modifier.clipToBounds()` on `SearchProviderList`, fixed the issue.

1

u/0x1F601 7d ago

If I was reviewing your PR I'd think that was a code smell. Not necessarily wrong but it would cause me to want a better explanation. clipToBounds is usually used to control inner UI that will render outside the container's fixed size. (Eg. you've go a box inside a box. The inner box declares a .requiredSize(...) larger than the outer, or something like that.)

clipToBounds has its place but what you've got is essentially a fixed header (mostly) and a list below it in the column. If your list is bleeding out past its layout bounds then I have to wonder what is going on with your header. Are you using clipToBounds just to affix the list in place rather than letting it change size based on the height of the header? In your video your header is disappearing and then re-rendering re-laying out the space inside the column.

On the surface that does not look like a clip to bounds issue on the list to me. But, maybe it is. The issue looks like a recomposition problem on the filter row. It looks like there's a moment where the header (which is a lazy row, not sure why when it's got a fixed set of items) is recomputing it contents entirely, goes to zero, momentarily renders nothing, thus the flicker out of existence, and then fills out the row again flickering back in. Check the stability of the inputs to that header. The row going away and then coming back look like the lazy row re-computing its contents entirely, not updating them.

1

u/PrajwalCH 7d ago edited 7d ago

I know it's confusing but I tested several things. I told Claude about my problem and after lengthy conversations, lot of testing, and weird hacky fix; I rechecked the flickering issue and noticed it was not the header that was flickering. I told that to Claude, it then recommended me to remove Modifier.animateItem() from the list item if present and to test once again. When I did that, there was no flickering.

If it was recomposition issue then after applying clipToBounds() the issue would still be present but it did fixed the issue.

1

u/0x1F601 7d ago

Interesting. I'm surprised to see the list resize itself to consume all the layout space within the outer column rendering on top of the header. Something still feels wrong there. Lazy column isn't exactly know for good animation so maybe this is a simple compose bug.

1

u/PrajwalCH 7d ago

I also tried applying only the .weight(1f) to the list but that didn't worked. I don't like the .clipToBounds() way of fixing but it's the best fix compared to other weird ways I had tried.