Building UIs can feel easy until it’s time to subscribe to state changes and handle side-effects effectively. Many developers overuse collectAsState everywhere, leading to lags and unexpected recompositions. Others have heard of snapshotFlow but don’t understand why it’s needed if StateFlow and collectAsState already exist.
In this small article, I share my perspective on when it makes sense to use snapshotFlow and when collectAsState is a better fit by exploring short, practical examples from real projects, helping you avoid hidden bugs and performance issues in projects.
Let’s break it down.
What collectAsState does
collectAsState subscribes to a Flow within Compose and automatically exposes it as State for easy display in the UI:
val uiState by viewModel.uiStateFlow.collectAsState() Text(uiState.text)
Key points:
- Very easy to use.
- Automatically cancels and restarts collection on recomposition.
- Ideal for ViewModel → UI data binding.
But:
- Triggers recomposition on every new emission, even if only a minor change occurs.
- Starts collecting as soon as the composable enters composition.
- Not suitable for observing Compose-specific states like scroll or gestures.
What snapshotFlow does
snapshotFlow converts Compose states (e.g., LazyListState, derivedStateOf) into a cold Flow, allowing you to react to state changes without unnecessary recompositions:
val listState = rememberLazyListState()
LaunchedEffect(Unit) {
snapshotFlow { listState.firstVisibleItemIndex }
.distinctUntilChanged()
.collect { index ->
analytics.logScrollPosition(index)
}
}
Key points:
- Ideal for side-effects on Compose state changes.
- Does not trigger recompositions.
- Used within
LaunchedEffector coroutines.
But:
- Does not expose
Statefor direct UI rendering. - Does not replace
collectAsStatefor ViewModel → UI updates.
When to use collectAsState
- Subscribing to
FloworStateFlowfrom your ViewModel for the UI. - Displaying data in the UI (text, loading states, fetched data).
- Low-frequency updates that the user needs to see.
Avoid using for:
- High-frequency updates (scroll offsets, sensor data).
- Triggering side-effects that don’t require UI updates.
When to use snapshotFlow
- Reacting to Compose states (scroll, gestures, animations).
- Triggering side-effects without causing recompositions.
- Building
Flowpipelines from Compose states (analytics, lazy loading triggers).
Avoid using for:
- Direct UI data rendering.
- Replacing
collectAsStatefor ViewModel → UI flows.
Practical examples for snapshotFlow
Wrong: Using snapshotFlow.collectAsState for animation progress
val progress by snapshotFlow { animationState.progress }
.collectAsState(initial = 0f)
Text("Progress: ${(progress * 100).toInt()}%")
Using snapshotFlow together with collectAsState to drive UI updates for animation progress causes recompositions on every frame, leading to jank and defeating the purpose of snapshotFlow.
Correct: Using snapshotFlow for analytics during animation
LaunchedEffect(Unit) {
snapshotFlow { animationState.progress }
.distinctUntilChanged { old, new ->
(old * 100).toInt() == (new * 100).toInt()
}
.collect { progress ->
analytics.logAnimationProgress(progress)
}
}
This tracks animation progress for analytics or logging without triggering UI recompositions.
Practical examples for collectAsState
Wrong: Using collectAsState for high-frequency scroll data
val scrollOffset by viewModel.scrollOffsetFlow.collectAsState()
Text("Offset: $scrollOffset")
This triggers recomposition on every pixel of scroll, overloading the CPU.
Correct: Using collectAsState for meaningful UI data
val userName by viewModel.userNameFlow.collectAsState()
Text("Hello, $userName!")
This is appropriate for displaying data that the user needs to see and that changes infrequently.
Job Offers
Conclusion
collectAsState and snapshotFlow complement each other:
- Use collectAsState to display ViewModel data in the UI.
- Use snapshotFlow to react to Compose state changes for side-effects without triggering recompositions.
Using them correctly will help you avoid unnecessary recompositions, improve your app’s responsiveness, and keep your Compose code clean, scalable, and predictable.
If you found this breakdown helpful, feel free to follow me for more insights.

Hands-on insights into mobile development,
engineering, and team leadership.
📬 Follow me on Medium
This article was previously published on proandroiddev.com.


