$npx -y skills add skydoves/compose-performance-skills --skill using-efficient-effectsUse this skill to choose the cheapest correct effect API in Jetpack Compose — LaunchedEffect, DisposableEffect, SideEffect, rememberUpdatedState, and skydoves/compose-effects' RememberedEffect and ViewModelStoreScope. Covers stale-callback bugs in long-lived `Launched
| 1 | # Using Efficient Effects — pick the cheapest correct effect API |
| 2 | |
| 3 | Compose ships three effect APIs by default: `LaunchedEffect` (coroutine, restarts on key change), `DisposableEffect` (setup/teardown with a cleanup block), and `SideEffect` (runs on every successful composition). The `skydoves/compose-effects` library adds `RememberedEffect` — a non-coroutine analog of `LaunchedEffect`. Picking the wrong API wastes a coroutine scope, leaks a listener, or silently drops the latest version of a callback. This skill is the decision tree. |
| 4 | |
| 5 | ## When to use this skill |
| 6 | |
| 7 | - The user asks which effect API to choose, or why a `LaunchedEffect` restarts unexpectedly. |
| 8 | - A long-lived `LaunchedEffect` calls a stale version of a callback that the parent has updated. |
| 9 | - A non-Compose subscriber (listener, observer, broadcast receiver, GL surface) needs setup at composition entry and teardown on key change or exit. |
| 10 | - A composable just needs to react to a key change synchronously and a coroutine scope is overkill. |
| 11 | - A `LazyColumn` row needs its own `ViewModel` instance (e.g. one VM per chat row, one VM per feed card) and the parent VM store would leak state across rows. |
| 12 | - The user is debugging unexpected effect restarts, leaked listeners, or per-frame allocations from `SideEffect`. |
| 13 | |
| 14 | ## When NOT to use this skill |
| 15 | |
| 16 | - Pure derivation of one `State<T>` from another — use `derivedStateOf`. See `../../recomposition/choosing-derivedstateof/SKILL.md`. |
| 17 | - Collecting a flow that originates outside the composition — use `collectAsStateWithLifecycle`. See `../collecting-flows-safely/SKILL.md`. |
| 18 | - One-shot computation that must survive recomposition without re-running — use `remember { ... }`, not `LaunchedEffect(Unit)`. |
| 19 | |
| 20 | ## Prerequisites |
| 21 | |
| 22 | - Familiarity with Compose's composition lifecycle: enter, recompose, leave. Effects run after composition commits. |
| 23 | - Compose UI 1.4+. |
| 24 | - For `RememberedEffect`, add `com.github.skydoves:compose-effects` (latest GA — see https://github.com/skydoves/compose-effects). |
| 25 | - For `ViewModelStoreScope`, also depend on `com.github.skydoves:compose-effects-viewmodel`. Import path: `com.skydoves.compose.effects.viewmodel.ViewModelStoreScope`. |
| 26 | - For `ViewModelStoreScope` with Hilt, also `androidx.hilt:hilt-navigation-compose` for `hiltViewModel()`. |
| 27 | |
| 28 | ## Workflow — decision tree |
| 29 | |
| 30 | 1. **Need a coroutine?** → `LaunchedEffect(key1, key2, ...)`. The block is launched in the composition's `CoroutineScope` and cancelled/relaunched whenever any key changes. Use for network calls, animations, suspending work. |
| 31 | 2. **Need setup + cleanup with no coroutine?** → `DisposableEffect(key) { onDispose { ... } }`. The `onDispose` block runs on key change AND when the composable leaves composition. Use for listeners, observers, manual subscriptions. |
| 32 | 3. **Need to react synchronously to a key change with no coroutine?** → `RememberedEffect(key) { ... }` from `skydoves/compose-effects`. Cheaper than spinning a `LaunchedEffect` scope just to call a synchronous function. No `onDispose` — pair with `DisposableEffect` if you need teardown. |
| 33 | 4. **Long-lived `LaunchedEffect` that consumes a callback that may change?** → wrap the callback with `val latest by rememberUpdatedState(callback)` and reference `latest` inside the effect. The effect keeps the same coroutine; the callback reference stays fresh. |
| 34 | 5. **Per-composable `ViewModel` (one per row in a `LazyColumn`, etc.)?** → wrap the row body in `ViewModelStoreScope(key = <stableId>) { ... }` from `com.github.skydoves:compose-effects-viewmodel` and call `hiltViewModel()` (or `viewModel()`) inside. The `key` is required and must be a stable identifier (the row's id, etc.); each key gets its own store. |
| 35 | 6. **Side effect that must run after every successful composition?** → `SideEffect { ... }`. Rare. Use for publishing Compose state to a non-Compose system that you cannot subscribe to via `DisposableEffect` (e.g. updating a `View`'s field on every commit). Avoid for per-frame work — it really does run every commit. |
| 36 | |
| 37 | ## Patterns |
| 38 | |
| 39 | ### Pattern: stale callback insi |