Loading...
Loading...
Use when writing or reviewing Jetpack Compose code with LaunchedEffect, DisposableEffect, SideEffect, rememberCoroutineScope, rememberUpdatedState, snapshotFlow, snackbar, navigation, focus requests, analytics, or event Flow collection.
npx skill4agent add chrisbanes/skills compose-side-effects| Need | API |
|---|---|
| Publish Compose state to non-Compose code after every successful recomposition | |
| Register/unregister a listener, callback, observer, or resource | |
| Run suspending, deferred, or keyed one-shot work | |
| Launch suspending work from a user event callback | |
| Convert Compose snapshot reads into a Flow inside a coroutine | |
// ✅ 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) }
}userIdscreenIdlifecycleOwnerfocusRequesterstateviewModelrememberUpdatedState@Composable
fun Timeout(onTimeout: () -> Unit) {
val latestOnTimeout by rememberUpdatedState(onTimeout)
LaunchedEffect(Unit) {
delay(1_000)
latestOnTimeout()
}
}onTimeoutonStartonStoprememberUpdatedState// 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) }
}rememberUpdatedStateStatecompose-state-deferred-readsLaunchedEffectLaunchedEffect(events) {
events.collect { event ->
snackbarHostState.showSnackbar(event.message)
}
}collectAsStateWithLifecycle()collectAsState()compose-state-holder-ui-splitcollectAsState()snapshotFlowLaunchedEffect(listState) {
snapshotFlow { listState.firstVisibleItemIndex }
.distinctUntilChanged()
.collect { index -> analytics.visibleIndex(index) }
}snapshotFlow { ... }.map { ... }collectrememberCoroutineScope()@Composable
fun SaveButton(snackbarHostState: SnackbarHostState) {
val scope = rememberCoroutineScope()
Button(
onClick = {
scope.launch {
snackbarHostState.showSnackbar("Saved")
}
},
) {
Text("Save")
}
}LaunchedEffectDisposableEffect@Composable
fun ObserveLifecycle(owner: LifecycleOwner, observer: LifecycleObserver) {
DisposableEffect(owner, observer) {
owner.lifecycle.addObserver(observer)
onDispose {
owner.lifecycle.removeObserver(observer)
}
}
}onDispose| Mistake | Fix |
|---|---|
| Network request directly in the composable body | Usually move to a ViewModel/state holder; use |
| Analytics property written from the composable body | Use |
| Impression/event logged from the composable body | Use |
| Key by |
| Hidden lifecycle bug |
| Long-lived effect invokes an old callback after recomposition | Stale capture |
| Key by the specific property |
| Usually |
Listener added in | Use |
Launching from click by setting | Use |
LaunchedEffect(Unit)rememberUpdatedState