Side Effects & the Compose Lifecycle
Why side effects need special tools
A composable can recompose many times per second, in any order. So if you start a network call or a timer directly in its body, it could fire dozens of times or leak. Side-effect APIs let you run such work in a controlled, lifecycle-aware way.
LaunchedEffect — run when keys change
Runs a coroutine when the composable first appears, and again whenever a key changes. Perfect for loading data for a given id.
@Composable
fun Profile(userId: Int, vm: ProfileViewModel) {
LaunchedEffect(userId) {
vm.load(userId) // re-runs only when userId changes
}
// ... UI ...
}
rememberCoroutineScope — launch from events
For coroutines started by a user action (a tap), not by entering the screen:
val scope = rememberCoroutineScope()
val snackbar = remember { SnackbarHostState() }
Button(onClick = {
scope.launch { snackbar.showSnackbar("Saved!") }
}) { Text("Save") }
DisposableEffect — set up and clean up
When you must register something and then remove it (a sensor listener, a callback), use DisposableEffect with an onDispose block.
DisposableEffect(Unit) {
val listener = LocationListener { /* ... */ }
locationManager.register(listener)
onDispose { locationManager.unregister(listener) } // cleanup
}
rememberUpdatedState & derivedStateOf (briefly)
rememberUpdatedState— reference the latest value inside a long-running effect without restarting it.derivedStateOf— compute a value from other state efficiently (recomputes only when inputs change).
Collecting state from a ViewModel
val uiState by viewModel.state.collectAsStateWithLifecycle()
Common mistakes
- Starting coroutines or network calls directly in the composable body.
- Using a constant key in
LaunchedEffectwhen it should depend on changing data. - Forgetting
onDisposecleanup inDisposableEffect(leaks).
Summary: Never run effects in the composable body. UseLaunchedEffectfor work tied to keys,rememberCoroutineScopefor event-driven coroutines, andDisposableEffectwhen you must clean up.