On this page
A side effect is any change to the world outside a composable's own UI: a network call, an analytics event, a registered listener, a snackbar. Because composables may run many times, in any order, or be skipped, side effects in their bodies run unpredictably. The effect APIs give you control.
Which effect API?
| API | Runs | Cleanup | Use for |
|---|---|---|---|
LaunchedEffect(keys) | A coroutine on entering composition; restarts when keys change | Coroutine cancelled on leave or restart | Suspend work tied to the composable: snackbars, animations, collecting events |
rememberCoroutineScope() | When you launch from a callback | Scope cancelled on leave | Coroutines started by user events (onClick โ scroll) |
DisposableEffect(keys) | On enter; re-runs when keys change | onDispose { } required | Register/unregister listeners, observers, callbacks |
SideEffect | After every successful recomposition | None | Publishing Compose state to non-Compose objects |
rememberUpdatedState | - | - | Read the latest lambda or value inside a long-running effect |
produceState | Coroutine producing a State | Cancelled on leave | Converting non-Compose sources into State |
snapshotFlow | - | - | Turning Compose state reads into a Flow |
LaunchedEffect
@Composable
fun NotesScreen(state: NotesUiState, onMessageShown: () -> Unit) {
val snackbarHostState = remember { SnackbarHostState() }
state.userMessage?.let { message ->
LaunchedEffect(message) { // restarts only for a new message
snackbarHostState.showSnackbar(message)
onMessageShown()
}
}
Scaffold(snackbarHost = { SnackbarHost(snackbarHostState) }) { padding -> /* ... */ }
}rememberCoroutineScope: effects from callbacks
val listState = rememberLazyListState()
val scope = rememberCoroutineScope()
FloatingActionButton(onClick = {
scope.launch { listState.animateScrollToItem(0) } // launched by an event, not by composition
}) { Icon(Icons.Default.KeyboardArrowUp, contentDescription = "Scroll to top") }You can't call LaunchedEffect inside onClick, because callbacks aren't composable. Use the remembered scope instead.
DisposableEffect: register and clean up
@Composable
fun TrackScreenTime(screenName: String, onTimeSpent: (String, Long) -> Unit) {
val lifecycleOwner = LocalLifecycleOwner.current
val currentOnTimeSpent by rememberUpdatedState(onTimeSpent) // always call the latest lambda
DisposableEffect(lifecycleOwner, screenName) {
var resumedAt = 0L
val observer = LifecycleEventObserver { _, event ->
when (event) {
Lifecycle.Event.ON_RESUME -> resumedAt = SystemClock.elapsedRealtime()
Lifecycle.Event.ON_PAUSE -> currentOnTimeSpent(screenName, SystemClock.elapsedRealtime() - resumedAt)
else -> Unit
}
}
lifecycleOwner.lifecycle.addObserver(observer)
onDispose { lifecycleOwner.lifecycle.removeObserver(observer) } // always paired
}
}Lifecycle-runtime-compose also offers LifecycleResumeEffect and LifecycleStartEffect for these common cases.
rememberUpdatedState: avoid stale lambdas
@Composable
fun SplashTimeout(onTimeout: () -> Unit) {
val latestOnTimeout by rememberUpdatedState(onTimeout)
LaunchedEffect(Unit) { // don't restart the timer when the lambda changes...
delay(2_000)
latestOnTimeout() // ...but still call the newest one
}
}SideEffect and produceState
SideEffect { analytics.setUserProperty("theme", if (isDark) "dark" else "light") }@Composable
fun rememberBatteryLevel(context: Context): State<Int?> = produceState<Int?>(initialValue = null, context) {
val receiver = object : BroadcastReceiver() {
override fun onReceive(c: Context, i: Intent) { value = i.getIntExtra(BatteryManager.EXTRA_LEVEL, -1) }
}
ContextCompat.registerReceiver(context, receiver, IntentFilter(Intent.ACTION_BATTERY_CHANGED), ContextCompat.RECEIVER_NOT_EXPORTED)
awaitDispose { context.unregisterReceiver(receiver) }
}Real-world use cases
Load data for an ID
LaunchedEffect(id) { viewModel.load(id) }, or better, let the ViewModel load from SavedStateHandle in init.
Location or sensor updates
DisposableEffect, or lifecycle effects, to start on resume and stop on pause.
One-time onboarding hint
LaunchedEffect(Unit) to show it, plus persistence so it doesn't show again.
Common mistakes
- Wrong keys:
LaunchedEffect(Unit)when the effect depends on a changing ID, or keys that change every recomposition (new objects), which restarts constantly. - Forgetting
onDisposecleanup, which leaks listeners. - Business logic in effects that belongs in the ViewModel.
- Writing state in
SideEffect, triggering endless recompositions. - Collecting flows in
LaunchedEffectwithout lifecycle awareness. UsecollectAsStateWithLifecycleorrepeatOnLifecycle.
Frequently asked questions
Is LaunchedEffect(Unit) like onCreate?
Roughly, but per composition entry, not per Activity. It runs again if the composable leaves and re-enters composition.
Do effects survive configuration changes?
No. The Activity is recreated, so composition restarts and effects run again. Keep long-running work in the ViewModel.
Can I pass multiple keys?
Yes, for example LaunchedEffect(userId, filter). The effect restarts when any key changes.
Conclusion
Keep composable bodies pure and move work into the right effect: LaunchedEffect for suspend work tied to composition, rememberCoroutineScope for event-driven coroutines, DisposableEffect for register and cleanup, SideEffect for publishing state outward. Choose keys deliberately.
Next step: make components reusable and testable with State Hoisting.
Official references
๐ฑ Apps built by Yunok Tech
Real Android apps on Google Play, free to download.
Walzi4K wallpapers for lock and home screenGet it โธ
NotiVaultNotification history: recover dismissed notificationsGet it โธ
Ready to build real Android applications?
Jetpack Compose clicks once you build real screens with real state. Practise modern Android UI development on projects that go beyond toy examples.
Move beyond theory and strengthen your Android development skills with Yunoktech's Android course. Learn core concepts, practical implementation, modern Android development techniques, and the reasoning behind building maintainable applications.
Explore the Android course and take your next step as an Android developer.