Blog Infos
Author
Published
Topics
, , ,
Published

Photo by Anton Savinov on Unsplash

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., LazyListStatederivedStateOf) 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 LaunchedEffect or coroutines.

But:

  • Does not expose State for direct UI rendering.
  • Does not replace collectAsState for ViewModel → UI updates.
When to use collectAsState
  • Subscribing to Flow or StateFlow from 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 Flow pipelines from Compose states (analytics, lazy loading triggers).

Avoid using for:

  • Direct UI data rendering.
  • Replacing collectAsState for 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

Job Offers

There are currently no vacancies.

OUR VIDEO RECOMMENDATION

No results found.

Jobs

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.

Menu