What you'll learn
Composables should be pure — describe UI from state, nothing else. But sometimes you need to reach outside that world: start a network load when a screen opens, register a listener, show a one-off snackbar. Those are side effects, and Compose has dedicated tools to run them safely in a world where functions re-run constantly.
By the end you'll be able to:
- Explain why side effects need special handling in Compose
- Run suspend work on entry with
LaunchedEffect - Launch coroutines from events with
rememberCoroutineScope - Register and clean up with
DisposableEffect, and optimize withderivedStateOf
Why effects need care
Recall that a composable can recompose many times — and you can't predict when or how often. So putting an action directly in the body is a bug: it fires on every recomposition.
@Composable
fun Screen(viewModel: MyViewModel) {
// ❌ WRONG: this runs on EVERY recomposition — many times!
viewModel.loadData()
Text("…")
}The fix is an effect handler: a composable that runs your side effect in a controlled way — once on entry, keyed to specific values, with proper cleanup. Never call functions with side effects directly in a composable's body.
LaunchedEffect
LaunchedEffect(key) runs a coroutine when the composable first enters the composition, and re-runs it only when a key changes (cancelling the previous run). It's the right place to kick off a load, start an animation, or observe something when a screen appears:
@Composable
fun ArticleScreen(id: Int, viewModel: ArticleViewModel) {
// ✅ Runs when the screen enters, and again only if 'id' changes.
LaunchedEffect(id) {
viewModel.load(id) // safe place to start a suspend call
}
val state by viewModel.uiState.collectAsStateWithLifecycle()
// …render state…
}Key idea
LaunchedEffect(id) means "redo this when id changes." Use LaunchedEffect(Unit) for run-once-on-entry. Get the key wrong and you either miss updates or re-fetch endlessly. (In practice, one-shot loads often live in the ViewModel's init instead — but keyed loads belong here.)rememberCoroutineScope
LaunchedEffect is for effects tied to composition. When you need to launch a coroutine from a callback — a button's onClick — you can't call suspend code directly. Grab a scope with rememberCoroutineScope() and launch into it:
@Composable
fun UndoBar(snackbarHostState: SnackbarHostState) {
val scope = rememberCoroutineScope() // tied to this composable
Button(onClick = {
scope.launch { // launch from an EVENT, not composition
snackbarHostState.showSnackbar("Deleted")
}
}) { Text("Delete") }
}The distinction is simple:
| Tool | Launch from | Example |
|---|---|---|
LaunchedEffect | Composition (entering / key change) | Load data when the screen opens |
rememberCoroutineScope | An event callback | Show a snackbar on button tap |
DisposableEffect
Some effects need cleanup — a listener you registered, an observer you attached. Leave them running after the composable is gone and you leak resources. DisposableEffect pairs setup with an onDispose block that runs when the composable leaves (or the key changes):
@Composable
fun rememberIsOnline(): State<Boolean> {
val isOnline = remember { mutableStateOf(true) }
val context = LocalContext.current
DisposableEffect(context) {
val callback = networkCallback { online -> isOnline.value = online }
connectivityManager.registerCallback(callback)
onDispose { // cleanup when leaving composition
connectivityManager.unregisterCallback(callback)
}
}
return isOnline
}derivedStateOf & produceState
Two more tools worth knowing. derivedStateOf computes a value from other state but only triggers recomposition when the result changes — perfect for "show a scroll-to-top button once you've scrolled", which would otherwise recompose on every scrolled pixel:
val listState = rememberLazyListState()
// Recompute ONLY when the boolean result flips — not on every scroll pixel.
val showButton by remember {
derivedStateOf { listState.firstVisibleItemIndex > 0 }
}And produceState conveniently turns a non-Compose async source into Compose State, launching a coroutine to feed it. Alongside SideEffect (for publishing Compose state to non-Compose code on every successful recomposition), these complete the effect toolkit — but LaunchedEffect and rememberCoroutineScope cover the vast majority of real needs.
Recap & quick check
Key takeaways
- Composables recompose often, so never call side-effecting code directly in the body.
- LaunchedEffect(key) runs a coroutine on entry and re-runs when the key changes — for loads tied to composition.
- rememberCoroutineScope() gives a scope to launch coroutines from event callbacks like onClick.
- DisposableEffect pairs setup with onDispose cleanup for listeners and observers.
- derivedStateOf recomputes only when its result changes; produceState/ SideEffect round out the toolkit.
Quick check
1. Why not call viewModel.load() directly in a composable's body?
2. Which effect runs a coroutine when a screen enters and re-runs when a key changes?
3. You need to show a snackbar when a button is tapped. Use…
4. What does DisposableEffect add that LaunchedEffect doesn't?
5. derivedStateOf is useful because it…
With effects under control, let's make the UI delightful — animations in Compose.