Collecting Flows Safely
Use this skill to migrate Compose UI from `collectAsState()` to `collectAsStateWithLifecycle()`, hoist `Flow<T>` parameters out of composables, and apply `.conflate()` / `.distinctUntilChanged()` / `snapshotFlow` so background CPU and battery stop draining and chatty flows stop invalidating the UI per emission. Covers ViewModel `StateFlow`/`SharedFlow` consumers, sensor and location streams, and the "Flow as composable parameter" antipattern. Trigger when the user mentions `collectAsState`, `collectAsStateWithLifecycle`, lifecycle-aware flow collection, `Lifecycle.State.STARTED`, background battery drain from a Compose screen, `snapshotFlow`, `Flow` parameter on a composable, conflate, or distinctUntilChanged.
MCP get_skill({ skillId: "collecting-flows-safely-4ddd6894" })Use this skill with your agent
Create a free account and connect via MCP
# Collecting Flows Safely — keep upstream work tied to the UI lifecycle
`collectAsState()` keeps collecting whenever the composable is in composition, including while the app is backgrounded — that wastes CPU and battery. `collectAsStateWithLifecycle()` ties collection to a `Lifecycle.State` (default `STARTED`) so backgrounding pauses upstream work. For chatty flows, pair the consumer with `.conflate()` and `.distinctUntilChanged()` so Compose only sees meaningful changes. Skydoves hot take #4: a `Flow<T>` parameter on a composable is unstable and blocks skipping for the whole composable — collect at the caller and pass the resolved value.
## When to use this skill
- A `ViewModel` or `Repository` exposes `StateFlow<T>`, `SharedFlow<T>`, or a cold `Flow<T>` to a Compose screen.
- The user reports background battery drain, "the screen keeps working when the app is in the recents tray", or wakelock noise on logcat.
- A composable consumes sensor data, location, GPS, websocket, or animation frames coming from outside Compose.
- A composable's signature is `fun MyScreen(state: Flow<State>)` (Flow-as-parameter antipattern).
- The user asks about `collectAsState` vs `collectAsStateWithLifecycle`, or about `Lifecycle.State.STARTED` / `RESUMED` semantics.
- The user wants Compose state to drive a non-Compose subscriber (analytics on visible item index, etc.) — that is the State→Flow direction served by `snapshotFlow`.
## When NOT to use this skill
- The flow is constructed and consumed entirely inside one composable's `remember { ... }` block — that is composition-internal state, prefer `mutableStateOf` directly. See `../using-efficient-effects/SKILL.md` for choosing the right effect API.
- The data source is already `State<T>` (e.g. `mutableStateOf`, `Animatable.asState()`) — do not wrap it in a flow just to call `collectAsStateWithLifecycle()`.
- A truly always-on background listener that must run while the Activity is `STOPPED`. Move that work to a `Service` / `WorkManager` / `repeatOnLifecycle` in the Activity, not into Compose.
## Prerequisites
- Compose UI 1.4+ (any modern release).
- Add `androidx.lifecycle:lifecycle-runtime-compose` (2.6+; the artifact that exposes `collectAsStateWithLifecycle`). Maven coordinates: `androidx.lifecycle:lifecycle-runtime-compose:<version>`.
- Kotlin coroutines basics (`StateFlow`, `SharedFlow`, `conflate`, `distinctUntilChanged`).
- Read `../../recomposition/deferring-state-reads/SKILL.md` if the high-frequency emissions are driving animation values — phase deferral may be a better fix than `.conflate()`.
## Workflow
1. **Audit every `collectAsState()` call.** Search the module for `\.collectAsState\(`. Replace each call with `collectAsStateWithLifecycle()` unless the producing flow is created inside the same composable scope.
2. **Provide an `initialValue`** when the flow is not a `StateFlow` (cold `Flow<T>` or `SharedFlow<T>`). For `StateFlow<T>`, the overload reads `.value` — no initial value needed.
3. **Pick the right `minActiveState`.** Default `STARTED` matches the framework's `repeatOnLifecycle` default. Use `RESUMED` for widgets that should only collect while the Activity owns input focus (e.g. always-visible foreground HUD with an aggressive sensor source). Do not use `CREATED` — that defeats the purpose.
4. **For high-frequency producers (>1 emission per ~100 ms)**, add `.conflate()` upstream of `collectAsStateWithLifecycle()` so the consumer keeps only the latest value across a frame. If consecutive emissions can be value-equal, also add `.distinctUntilChanged()` — and ensure the emitted type has a correct `equals()`.
5. **Hoist `Flow<T>` parameters out of composables.** Replace `fun Foo(prices: Flow<Price>)` with `fun Foo(price: Price)`. Collect at the caller. If the producer must stay private to the parent, expose a `() -> Price` lambda provider rather than the raw `Flow`.
6. **For State → Flow direction**, use `snapshotFlow { ... }` inside a `LaunchedEffect`. That is the supported bridge from Compose's snapshot system to coroutine flows; combine with `.distinctUntilChanged()` to avoid spurious emissions.
7. **Verify** with `@TraceRecomposition` (see `../../measurement/tracing-recompositions-at-runtime/SKILL.md`) and a logcat sanity check with the app backgrounded — upstream emissions should stop.
## Patterns
### Pattern: replace `collectAsState` with `collectAsStateWithLifecycle`
```kotlin
// WRONG
import androidx.compose.runtime.collectAsState
@Composable
fun HomeScreen(viewModel: HomeViewModel) {
val state by viewModel.uiState.collectAsState()
HomeContent(state)
}
// WRONG because: collection continues while the app is backgrounded -> wasted CPU and battery,
// and any upstream operators (network polling, db queries) keep running.
```
```kotlin
// RIGHT
import androidx.lifecycle.compose.collectAsStateWithLifecycle
@Composable
fun HomeScreen(viewModel: HomeViewModel) {
val state by viewModel.uiState.collectAsStateWithLifecycle()
HomeContent(state)
}
```
### Pattern: custom `minActiveState` for an always-visible widget
```kotlin
// RIGHT
import androidx.lifecycle.Lifecycle
import androidx.lifecycle.compose.collectAsStateWithLifecycle
@Composable
fun TopBar(viewModel: HomeViewModel) {
val state by viewModel.uiState.collectAsStateWithLifecycle(
minActiveState = Lifecycle.State.RESUMED,
)
TopBarContent(state)
}
```
### Pattern: do not pass `Flow<T>` as a composable parameter (skydoves hot take #4)
```kotlin
// WRONG
@Composable
fun PriceTicker(prices: Flow<Price>) {
val price by prices.collectAsState(initial = Price.ZERO)
Text(price.formatted)
}
// WRONG because: Flow is unstable -> blocks skipping for the whole composable; the collection
// lifecycle is also unclear (recreated on every recomposition unless the caller remembers it).
```
```kotlin
// RIGHT
@Composable
fun PriceTicker(price: Price) {
Text(price.formatted)
}
// Caller collects once and passes the value:
@Composable
fun TickerScreen(viewModel: TickerViewModel) {
val price by viewModel.price.collectAsStateWithLifecycle()
PriceTicker(price)
}
```
### Pattern: high-frequency flow needs `.conflate()` + `.distinctUntilChanged()`
```kotlin
// WRONG
@Composable
fun TiltIndicator(repository: SensorRepository) {
val tilt by repository.tilt.collectAsStateWithLifecycle(initialValue = 0f)
TiltUi(tilt)
}
// WRONG because: hundreds of emissions per second invalidate the consuming composable per emission;
// most are dropped frames worth of work.
```
```kotlin
// RIGHT
@Composable
fun TiltIndicator(repository: SensorRepository) {
val flow = remember(repository) {
repository.tilt.conflate().distinctUntilChanged()
}
val tilt by flow.collectAsStateWithLifecycle(initialValue = 0f)
TiltUi(tilt)
}
```
### Pattern: State → Flow with `snapshotFlow`
```kotlin
// RIGHT
@Composable
fun FeedAnalytics(listState: LazyListState, analytics: Analytics) {
LaunchedEffect(listState, analytics) {
snapshotFlow { listState.firstVisibleItemIndex }
.distinctUntilChanged()
.collect { index -> analytics.logFirstVisibleIndex(index) }
}
}
// snapshotFlow bridges snapshot State to a cold Flow without paying the cost of recomposing
// every time firstVisibleItemIndex changes — the read happens inside the LaunchedEffect's coroutine,
// not in the composition phase.
```
### Pattern: prefer `snapshotFlow` over `derivedStateOf` for fire-and-forget reactions
```kotlin
// LESS PREFERRED for an effect that only emits side effects
val isAtTop by remember { derivedStateOf { listState.firstVisibleItemIndex == 0 } }
LaunchedEffect(isAtTop) { if (isAtTop) reportTop() }
```
```kotlin
// PREFERRED — no derived state slot in composition; reads happen in the coroutine
LaunchedEffect(listState) {
snapshotFlow { listState.firstVisibleItemIndex == 0 }
.distinctUntilChanged()
.filter { it }
.collect { reportTop() }
}
```
## Mandatory rules
- **MUST** call `collectAsStateWithLifecycle()` (from `androidx.lifecycle:lifecycle-runtime-compose`) for any `Flow` that originates outside the composition (ViewModel, repository, sensor, network, websocket, location).
- **MUST NOT** pass `Flow<T>` as a composable parameter. Collect at the caller, pass the resolved `T` (or a `() -> T` lambda provider for very hot producers). Flow is unstable and blocks skipping for the entire composable.
- **MUST** add `.conflate()` and/or `.distinctUntilChanged()` upstream of `collectAsStateWithLifecycle()` for any flow emitting more often than ~once per 100 ms. Ensure `equals()` is correct on the emitted type when using `distinctUntilChanged`.
- **MUST** wrap the operator chain in `remember(key)` when applying `.conflate()` / `.distinctUntilChanged()` inline, so a new chain is not created on every recomposition.
- **MUST NOT** use `Lifecycle.State.CREATED` for `minActiveState` — it leaves collection running while the activity is invisible, defeating the migration.
- **MUST NOT** rewrap an existing `State<T>` into a flow just to call `collectAsStateWithLifecycle()`. Keep the `State` direct.
- **PREFERRED:** `snapshotFlow { ... }` over `derivedStateOf { ... }` when the only consumer is a fire-and-forget effect (analytics, logging, side-channel emit) instead of UI.
## Verification
- [ ] `grep -R "collectAsState(" src/` returns 0 hits, or each remaining hit collects a flow created inside the same composable.
- [ ] Background the app and watch logcat: upstream operators (`Repository` log lines, sensor callbacks) stop within one frame and resume on foregrounding.
- [ ] `@TraceRecomposition(traceStates = true)` on the consuming composable shows one recomposition per *meaningful* emission, not per raw upstream emission. See `../../measurement/tracing-recompositions-at-runtime/SKILL.md`.
- [ ] Battery Historian / Studio Energy Profiler shows no foreground-only work attributed to the screen while the app is backgrounded.
- [ ] No composable in the module declares a parameter of type `Flow<*>` (`grep -R ": Flow<" src/`).
- [ ] When `minActiveState = RESUMED` is used, the rationale (focus-required widget) is documented in code.
## References
- Manuel Vivo — Consuming flows safely in Jetpack Compose: https://medium.com/androiddevelopers/consuming-flows-safely-in-jetpack-compose-cde014d0d5a3
- Android Developers — Lifecycle-aware coroutines (`repeatOnLifecycle`): https://developer.android.com/topic/libraries/architecture/coroutines
- `androidx.lifecycle:lifecycle-runtime-compose` release notes: https://developer.android.com/jetpack/androidx/releases/lifecycle
- Android Developers — Compose side effects (`snapshotFlow`, `LaunchedEffect`): https://developer.android.com/develop/ui/compose/side-effects
- Skydoves — 6 Jetpack Compose Guidelines (Flow-as-parameter antipattern): https://medium.com/proandroiddev/6-jetpack-compose-guidelines-to-optimize-your-app-performance-be18533721f9
- Ben Trengrove — Why test perf in release: https://medium.com/androiddevelopers/why-should-you-always-test-compose-performance-in-release-4168dd0f2c71
- Sibling skill: `../using-efficient-effects/SKILL.md` for `LaunchedEffect` / `DisposableEffect` / `RememberedEffect` selection.
- Sibling skill: `../../measurement/tracing-recompositions-at-runtime/SKILL.md` for verifying emission counts at runtime.Related Skills
More skills in Mobile App Development
Android Design Guidelines
Material Design 3 and Android platform guidelines. Use when building Android apps with Jetpack Compose or XML layouts, implementing Material You, navigation, or accessibility. Triggers on tasks involving Android UI, Compose components, dynamic color, or Material Design compliance.
Android Java Skill
Android Java development with MVVM, ViewBinding, and Espresso testing
Android Kotlin
Android Kotlin development with Coroutines, Jetpack Compose, Hilt, and MockK testing
Appium Skill
Generates production-grade Appium mobile automation scripts for Android and iOS in Java, Python, or JavaScript. Supports real device and emulator testing locally and on TestMu AI cloud with 100+ real devices. Use when the user asks to automate mobile apps, test on Android/iOS, write Appium tests, or mentions "Appium", "mobile testing", "real device", "app automation". Triggers on: "Appium", "mobile test", "Android test", "iOS test", "real device", "app automation", "UiAutomator", "XCUITest driver", "TestMu", "LambdaTest".
Apple Appstore Reviewer
Serves as a reviewer of the codebase with instructions on looking for Apple App Store optimizations or rejection reasons.
App Rejection Recovery
When the user's app or update was rejected by Apple App Review or Google Play Review and they need to diagnose why, fix it, and resubmit fast. Use when the user mentions "app rejected", "App Review rejection", "guideline violation", "Apple rejected my app", "Google Play rejected", "Play policy violation", "Resolution Center", "metadata rejection", "binary rejection", "guideline 2.1", "guideline 4.3", "guideline 5.1.1", "Sign in with Apple required", "Apple ID rejection", "Play Store suspension", "appeal", "I need to respond to App Review", or "expedited review". For pre-submission listing health, see aso-audit. For metadata-only fixes, see metadata-optimization.
Explore Other Categories
Skills from other categories with shared topics
Auditing Compose Performance
Use this skill to run an end-to-end Jetpack Compose performance audit when the symptom is broad ("the app feels sluggish", "scroll is rough everywhere", "we're starting a perf sprint", "what should we fix first?"). Orchestrates the four-phase Measure → Diagnose → Fix → Verify loop by sequencing the 25 focused skills (release-mode setup, R8, Baseline Profiles, Compose Compiler reports, stability inference, Layout Inspector, `@TraceRecomposition`, stabilization, strong skipping, phase-deferral, derivedStateOf, lazy layouts, lazy prefetch, Modifier.Node, modifier ordering, flow collection, effects, CI gates, hot-reload) and produces a written audit report with Before/After Macrobenchmark numbers. Use when the developer wants a perf sprint kickoff, a pre-release perf gate, onboarding to a perf-troubled codebase, or a written deliverable. Use when the user mentions "audit", "perf review", "perf sprint", "where do I start", or has no specific symptom yet.
Configuring R8 For Compose
Use this skill to configure R8 correctly for a Jetpack Compose application — full mode by default, `proguard-android-optimize.txt`, resource shrinking on, and minimal keep rules because Compose ships consumer ProGuard rules. Covers AGP 8.0+ R8 full mode default, R8's Compose-aware optimizations (lambda grouping, `sourceInformation()` stripping, composable arg constant-folding, `ComposerImpl` devirtualization), legitimate keep needs (`@Serializable`, Hilt entry points, reflective `Saver`s), and the AGP 8.x missing-rule reporter / R8 retrace. Cited gain is roughly 75 percent startup and 60 percent frame-render improvement debug-to-release. Use when setting up a new Compose app, when a PR adds an over-broad keep like `-keep class androidx.compose.** { *; }`, when a release build crashes after enabling minification, when APK size needs reduction, or when first enabling minification.
Stabilizing Compose Types
Use this skill to fix unstable Jetpack Compose types once a stability diagnosis has identified them. Covers the three-tier strategy — make the type truly stable with val plus immutable fields, mark with @Immutable or @Stable when the source is owned, and use stabilityConfigurationFiles for third-party or Java types. Explains the compiler-level difference between @Immutable and @Stable (static expression promotion), kotlinx.collections.immutable for List/Set/Map parameters, and the StableHolder wrapper escape hatch. Use when the developer asks how to stabilize a User class, a List parameter, java.time.LocalDateTime, a Flow parameter, or when the compiler report shows unstable params and the developer wants the fix. The diagnostic step lives in a sibling skill.