compose-side-effects
Use when writing or reviewing Jetpack Compose code with LaunchedEffect, DisposableEffect, SideEffect, rememberCoroutineScope, rememberUpdatedState, snapshotFlow, snackbar, navigation, focus requests, analytics, or event Flow collection.
What this skill does
# Compose: side effects
## Core principle
Composable bodies describe UI. They can be recomposed, skipped, or abandoned. Work that changes the outside world belongs in an effect API whose lifecycle matches the work.
## Pick the smallest effect
| Need | API |
|---|---|
| Publish Compose state to non-Compose code after every successful recomposition | `SideEffect` |
| Register/unregister a listener, callback, observer, or resource | `DisposableEffect(keys...)` |
| Run suspending, deferred, or keyed one-shot work | `LaunchedEffect(keys...)` |
| Launch suspending work from a user event callback | `rememberCoroutineScope()` |
| Convert Compose snapshot reads into a Flow inside a coroutine | `snapshotFlow { ... }` inside `LaunchedEffect` |
## Effect keys
Keys define restart identity. When any key changes, the old effect is cancelled/disposed and a new one starts.
```kotlin
// ✅ Restart collection when userId changes
LaunchedEffect(userId) {
repository.events(userId).collect { event -> handle(event) }
}
// ❌ Unit hides a changing input; collection keeps using the first userId
LaunchedEffect(Unit) {
repository.events(userId).collect { event -> handle(event) }
}
```
Use stable, semantic keys:
- Use the thing whose lifecycle the effect follows: `userId`, `screenId`, `lifecycleOwner`, `focusRequester`.
- Do not use broad objects (`state`, `viewModel`) when only one property matters.
- Do not add changing lambdas as keys unless you really want restarts on every lambda change.
## Avoid stale captures
For long-running effects that should not restart but need the latest callback or value, use `rememberUpdatedState`.
```kotlin
@Composable
fun Timeout(onTimeout: () -> Unit) {
val latestOnTimeout by rememberUpdatedState(onTimeout)
LaunchedEffect(Unit) {
delay(1_000)
latestOnTimeout()
}
}
```
Use this when the lifecycle is "start once" but the invoked lambda should stay fresh. Common cases:
- A timeout or splash effect should not restart when `onTimeout` changes, but it should call the latest callback.
- A lifecycle observer should stay registered to the same owner, but invoke the latest `onStart` / `onStop` lambdas.
- A long-running collector should keep its collection lifecycle, but call the latest event handler.
Do not use `rememberUpdatedState` to avoid choosing proper keys. If the changed value should restart the work, make it a key instead:
```kotlin
// BAD: userId changes should restart the collection, not update a captured value.
val latestUserId by rememberUpdatedState(userId)
LaunchedEffect(Unit) {
repository.events(latestUserId).collect { event -> handle(event) }
}
// GOOD: the collection lifecycle follows userId.
LaunchedEffect(userId) {
repository.events(userId).collect { event -> handle(event) }
}
```
### `rememberUpdatedState` values are stale inside `remember {}` blocks
`rememberUpdatedState` returns a `State` object whose `.value` is updated on every recomposition. The "latest" behavior only helps when the State is **read lazily** — inside an effect body or a lambda that runs later — not when the value is captured eagerly.
Inside a `remember {}` block the producer lambda runs once. Reading the delegate there snapshots the current `.value` into the remembered object — future State updates never reach it:
```kotlin
val latestChannelId by rememberUpdatedState(channelId)
// ❌ BAD — channelId is read once when remember's lambda executes;
// the destination holds the initial value forever
val destination = remember {
Destination(channelId = latestChannelId)
}
// ✅ GOOD — skip rememberUpdatedState; key remember on the changing value
val destination = remember(channelId) {
Destination(channelId = channelId)
}
// ✅ ALSO GOOD — wrapping lambda defers the read to each invocation
val destination = remember {
Destination(channelId = { latestChannelId })
}
```
The same trap applies anywhere a `rememberUpdatedState` delegate is **read eagerly** rather than deferred behind a lambda or effect body: data classes constructed in `remember`, objects built once in `DisposableEffect`'s setup block, or any expression evaluated at creation time.
When the captured value should trigger recreation of the remembered object, make it a `remember` key and skip `rememberUpdatedState` entirely. Reserve `rememberUpdatedState` for values that must stay fresh inside a long-lived scope (effect coroutine, event callback) **without** restarting that scope.
`rememberUpdatedState` also does not make render state "non-recomposing." If the UI needs to display a changing value, read normal `State` in composition or use the deferred-read patterns in [`compose-state-deferred-reads`](../compose-state-deferred-reads/SKILL.md) for frame-rate values.
## Collecting Flow
Use `LaunchedEffect` for **side-effect/event flows**: snackbars, navigation events, analytics events, focus commands, or other streams where each emission triggers imperative work.
```kotlin
LaunchedEffect(events) {
events.collect { event ->
snackbarHostState.showSnackbar(event.message)
}
}
```
Do not collect render state imperatively just to mutate local state. For UI state, collect near the state holder and pass plain values into the UI composable—the **state-holder vs UI split**, `collectAsStateWithLifecycle()` / `collectAsState()`, and preview-friendly wiring are covered in [`compose-state-holder-ui-split`](../compose-state-holder-ui-split/SKILL.md). Do not duplicate that architecture here.
On Android, prefer lifecycle-aware collection where available; use `collectAsState()` on targets without lifecycle-aware APIs.
For Compose state reads, use `snapshotFlow`:
```kotlin
LaunchedEffect(listState) {
snapshotFlow { listState.firstVisibleItemIndex }
.distinctUntilChanged()
.collect { index -> analytics.visibleIndex(index) }
}
```
`snapshotFlow { ... }.map { ... }` without a terminal `collect` does nothing.
## User events
Use `rememberCoroutineScope()` when a click or gesture starts suspending work:
```kotlin
@Composable
fun SaveButton(snackbarHostState: SnackbarHostState) {
val scope = rememberCoroutineScope()
Button(
onClick = {
scope.launch {
snackbarHostState.showSnackbar("Saved")
}
},
) {
Text("Save")
}
}
```
Avoid "event flag" state just to trigger a `LaunchedEffect`. The click already is the event.
## Registration and cleanup
Use `DisposableEffect` for paired setup/teardown:
```kotlin
@Composable
fun ObserveLifecycle(owner: LifecycleOwner, observer: LifecycleObserver) {
DisposableEffect(owner, observer) {
owner.lifecycle.addObserver(observer)
onDispose {
owner.lifecycle.removeObserver(observer)
}
}
}
```
Every registration path should have a matching `onDispose` cleanup path.
## Common mistakes
| Mistake | Diagnosis | Fix |
|---|---|---|
| Network request directly in the composable body | Side work in composition | Usually move to a ViewModel/state holder; use `LaunchedEffect` only for UI-owned keyed work |
| Analytics property written from the composable body | Side work in composition | Use `SideEffect` when it should publish after every successful recomposition |
| Impression/event logged from the composable body | Side work in composition | Use `LaunchedEffect(key)` when it should run once for that key |
| `LaunchedEffect(Unit)` captures changing `id` | Missing key | Key by `id`, or use `rememberUpdatedState` if it must not restart |
| `rememberUpdatedState(id)` used so `LaunchedEffect(Unit)` keeps running after `id` changes | Hidden lifecycle bug | Key the effect by `id` |
| Long-lived effect invokes an old callback after recomposition | Stale capture | Wrap the callback with `rememberUpdatedState` and call the wrapper inside the effect |
| `rememberUpdatedState` delegate read directly in `remember {}` (e.g. `Destination(id = latestId)`) | Value captured once, never Related in Data & Analytics
clawarr-suite
IncludedComprehensive management for self-hosted media stacks (Sonarr, Radarr, Lidarr, Readarr, Prowlarr, Bazarr, Overseerr, Plex, Tautulli, SABnzbd, Recyclarr, Unpackerr, Notifiarr, Maintainerr, Kometa, FlareSolverr). Deep library exploration, analytics, dashboard generation, content management, request handling, subtitle management, indexer control, download monitoring, quality profile sync, library cleanup automation, notification routing, collection/overlay management, and media tracker integration (Trakt, Letterboxd, Simkl).
querying-soql
IncludedSOQL query generation, optimization, and analysis with 100-point scoring. Use this skill when the user needs SOQL/SOSL authoring or optimization: natural-language-to-query generation, relationship queries, aggregates, query-plan analysis, and performance or safety improvements for Salesforce queries. TRIGGER when: user writes, optimizes, or debugs SOQL/SOSL queries, touches .soql files, or asks about relationship queries, aggregates, or query performance. DO NOT TRIGGER when: bulk data operations (use handling-sf-data), Apex DML logic (use generating-apex), or report/dashboard queries.
app-store-optimization
IncludedApp Store Optimization (ASO) toolkit for researching keywords, analyzing competitor rankings, generating metadata suggestions, and improving app visibility on Apple App Store and Google Play Store. Use when the user asks about ASO, app store rankings, app metadata, app titles and descriptions, app store listings, app visibility, or mobile app marketing on iOS or Android. Supports keyword research and scoring, competitor keyword analysis, metadata optimization, A/B test planning, launch checklists, and tracking ranking changes.
habit-flow
IncludedAI-powered atomic habit tracker with natural language logging, streak tracking, smart reminders, and coaching. Use for creating habits, logging completions naturally ("I meditated today"), viewing progress, and getting personalized coaching.
app-store-optimization
IncludedApp Store Optimization (ASO) toolkit for researching keywords, analyzing competitor rankings, generating metadata suggestions, and improving app visibility on Apple App Store and Google Play Store. Use when the user asks about ASO, app store rankings, app metadata, app titles and descriptions, app store listings, app visibility, or mobile app marketing on iOS or Android. Supports keyword research and scoring, competitor keyword analysis, metadata optimization, A/B test planning, launch checklists, and tracking ranking changes.
visualizing-data
IncludedBuilds dashboards, reports, and data-driven interfaces requiring charts, graphs, or visual analytics. Provides systematic framework for selecting appropriate visualizations based on data characteristics and analytical purpose. Includes 24+ visualization types organized by purpose (trends, comparisons, distributions, relationships, flows, hierarchies, geospatial), accessibility patterns (WCAG 2.1 AA compliance), colorblind-safe palettes, and performance optimization strategies. Use when creating visualizations, choosing chart types, displaying data graphically, or designing data interfaces.