$npx -y skills add skydoves/compose-performance-skills --skill collecting-flows-safelyUse 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 i
| 1 | # Collecting Flows Safely — keep upstream work tied to the UI lifecycle |
| 2 | |
| 3 | `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. |
| 4 | |
| 5 | ## When to use this skill |
| 6 | |
| 7 | - A `ViewModel` or `Repository` exposes `StateFlow<T>`, `SharedFlow<T>`, or a cold `Flow<T>` to a Compose screen. |
| 8 | - The user reports background battery drain, "the screen keeps working when the app is in the recents tray", or wakelock noise on logcat. |
| 9 | - A composable consumes sensor data, location, GPS, websocket, or animation frames coming from outside Compose. |
| 10 | - A composable's signature is `fun MyScreen(state: Flow<State>)` (Flow-as-parameter antipattern). |
| 11 | - The user asks about `collectAsState` vs `collectAsStateWithLifecycle`, or about `Lifecycle.State.STARTED` / `RESUMED` semantics. |
| 12 | - 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`. |
| 13 | |
| 14 | ## When NOT to use this skill |
| 15 | |
| 16 | - 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. |
| 17 | - The data source is already `State<T>` (e.g. `mutableStateOf`, `Animatable.asState()`) — do not wrap it in a flow just to call `collectAsStateWithLifecycle()`. |
| 18 | - 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. |
| 19 | |
| 20 | ## Prerequisites |
| 21 | |
| 22 | - Compose UI 1.4+ (any modern release). |
| 23 | - Add `androidx.lifecycle:lifecycle-runtime-compose` (2.6+; the artifact that exposes `collectAsStateWithLifecycle`). Maven coordinates: `androidx.lifecycle:lifecycle-runtime-compose:<version>`. |
| 24 | - Kotlin coroutines basics (`StateFlow`, `SharedFlow`, `conflate`, `distinctUntilChanged`). |
| 25 | - 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()`. |
| 26 | |
| 27 | ## Workflow |
| 28 | |
| 29 | 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. |
| 30 | 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. |
| 31 | 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. |
| 32 | 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()`. |
| 33 | 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`. |
| 34 | 6. **For State → Flow direction**, use `snapshotFlow { ... }` inside a `LaunchedEffe |