You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

Jetpack Compose中deriveStateOf与snapshotFlow协同使用时无法触发重组的问题

Why derivedStateOf and snapshotFlow Aren't Playing Nice in Your Jetpack Compose Code

Great question! Let's break down exactly why your second button stops showing when you switch to derivedStateOf, and how to fix this issue.

What's Going Wrong?

Let's start with the core difference between your working and broken code:

Original Code (Working)

Your initial implementation used remember(listState.firstVisibleItemIndex) to create a mutableStateOf for showButtonDerive:

val showButtonDerive by remember(listState.firstVisibleItemIndex) {
    mutableStateOf(listState.firstVisibleItemIndex > 0)
}

This setup forces the entire TestSnapshotFlow composable to recompose every time firstVisibleItemIndex changes—even if the result of listState.firstVisibleItemIndex > 0 stays the same. This over-recomposition ensures that whenever showButtonSnapshot is updated in your snapshotFlow collector, the next recomposition picks up the new value and updates the second button's visibility.

Broken Code (Using derivedStateOf)

When you switched to derivedStateOf:

val showButtonDerive by remember {
    derivedStateOf { listState.firstVisibleItemIndex > 0 }
}

Compose applies its fine-grained recomposition optimizations. derivedStateOf only triggers recomposition when its computed result changes (e.g., when firstVisibleItemIndex goes from 0 to 1, flipping showButtonDerive from false to true). Once showButtonDerive is true, further scrolls (like moving to index > 2) don't trigger a full recomposition of TestSnapshotFlow.

Even though your snapshotFlow correctly updates showButtonSnapshot to true, the second AnimatedVisibility doesn't update because:

  1. The full TestSnapshotFlow composable isn't recomposing (since showButtonDerive hasn't changed)
  2. The second AnimatedVisibility is nested directly in TestSnapshotFlow, and Compose's optimization doesn't trigger a targeted recomposition for it alone in this setup.

Fixes to Try

Here are three straightforward solutions to get your code working as expected:

1. Replace snapshotFlow with Another derivedStateOf

Since you're already using derivedStateOf for the first button, extend this pattern to the second button. This eliminates the need for snapshotFlow and mutableStateOf entirely, letting Compose handle all recomposition logic efficiently:

@ExperimentalAnimationApi
@Composable
fun TestSnapshotFlow() {
    Box(modifier = Modifier.fillMaxSize(), contentAlignment = Alignment.Center) {
        val listState = rememberLazyListState()
        LazyColumn(state = listState) {
            items(1000) { index ->
                Text(text = "Item: $index")
            }
        }
        
        // Use derivedStateOf for both buttons
        val showButtonDerive by remember {
            derivedStateOf { listState.firstVisibleItemIndex > 0 }
        }
        val showButtonSnapshot by remember {
            derivedStateOf { listState.firstVisibleItemIndex > 2 }
        }

        Log.d("Track", "Recompose")
        Column {
            AnimatedVisibility(showButtonDerive) {
                Button({}) {
                    Text("Row 1 hiding")
                }
            }
            AnimatedVisibility(showButtonSnapshot) {
                Button({}) {
                    Text("Row 1 and 2 hiding")
                }
            }
        }
    }
}

2. Force Recomposition on showButtonSnapshot Changes

If you want to keep using snapshotFlow, you can force TestSnapshotFlow to recompose whenever showButtonSnapshot changes by adding a dummy read of the state at the top level of the composable:

@ExperimentalAnimationApi
@Composable
fun TestSnapshotFlow() {
    Box(modifier = Modifier.fillMaxSize(), contentAlignment = Alignment.Center) {
        val listState = rememberLazyListState()
        LazyColumn(state = listState) {
            items(1000) { index ->
                Text(text = "Item: $index")
            }
        }
        var showButtonSnapshot by remember { mutableStateOf(false) }
        
        // Dummy read to trigger full recomposition when showButtonSnapshot changes
        val _ = showButtonSnapshot
        
        val showButtonDerive by remember {
            derivedStateOf { listState.firstVisibleItemIndex > 0 }
        }

        Log.d("Track", "Recompose")
        Column {
            AnimatedVisibility(showButtonDerive) {
                Button({}) {
                    Text("Row 1 hiding")
                }
            }
            AnimatedVisibility(showButtonSnapshot) {
                Button({}) {
                    Text("Row 1 and 2 hiding")
                }
            }
        }
        LaunchedEffect(listState) {
            snapshotFlow { listState.firstVisibleItemIndex }
                .map { index -> index > 2 }
                .distinctUntilChanged()
                .collect {
                    Log.d("Track", "B $it")
                    showButtonSnapshot = it
                }
        }
    }
}

3. Extract the Second Button to a Separate Composable

Compose's targeted recomposition works best when you split your UI into smaller, focused composables. Extract the second button into its own composable, which directly reads showButtonSnapshot:

@ExperimentalAnimationApi
@Composable
fun TestSnapshotFlow() {
    Box(modifier = Modifier.fillMaxSize(), contentAlignment = Alignment.Center) {
        val listState = rememberLazyListState()
        LazyColumn(state = listState) {
            items(1000) { index ->
                Text(text = "Item: $index")
            }
        }
        var showButtonSnapshot by remember { mutableStateOf(false) }
        val showButtonDerive by remember {
            derivedStateOf { listState.firstVisibleItemIndex > 0 }
        }

        Log.d("Track", "Recompose")
        Column {
            AnimatedVisibility(showButtonDerive) {
                Button({}) {
                    Text("Row 1 hiding")
                }
            }
            // Use the separate composable
            SecondVisibilityButton(show = showButtonSnapshot)
        }
        LaunchedEffect(listState) {
            snapshotFlow { listState.firstVisibleItemIndex }
                .map { index -> index > 2 }
                .distinctUntilChanged()
                .collect {
                    Log.d("Track", "B $it")
                    showButtonSnapshot = it
                }
        }
    }
}

@ExperimentalAnimationApi
@Composable
private fun SecondVisibilityButton(show: Boolean) {
    AnimatedVisibility(show) {
        Button({}) {
            Text("Row 1 and 2 hiding")
        }
    }
}

This way, when showButtonSnapshot changes, only SecondVisibilityButton recomposes—no need for full composable recomposition, and the button will show up as expected.

Key Takeaway

derivedStateOf is a powerful tool to reduce unnecessary recompositions, but it requires you to think more carefully about how state changes propagate through your UI. The original remember with a key was causing over-recomposition, which accidentally masked the issue with snapshotFlow and mutableStateOf. By aligning your state handling with Compose's recomposition model (either using derivedStateOf for all derived states or splitting composables), you'll get both correct behavior and optimal performance.

内容的提问来源于stack exchange,提问作者Elye

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.04.28 10:27:28