derivedStateOf and Side Effects
Composable functions are meant to be side-effect-free: given the same inputs, they should describe the same UI. But real apps need to do things that fall outside pure UI description — launching a coroutine to fetch data, starting an animation once, registering a broadcast receiver, or subscribing to a Flow. Compose's 'effect handlers' — LaunchedEffect, DisposableEffect, SideEffect, and rememberCoroutineScope — are the sanctioned escape hatches for these effects, each tied to the composable's lifecycle so effects start and stop in a controlled, predictable way rather than leaking or re-running chaotically on every recomposition.
Cricket analogy: A commentator is meant to just describe the play, but sometimes needs to signal the third umpire for a review — a controlled, sanctioned break from pure description, like LaunchedEffect being the sanctioned escape hatch from a composable's pure UI description.
derivedStateOf for Computed State
derivedStateOf solves a narrower but very common problem: avoiding excess recomposition when a value is computed from other state but changes less often than its inputs. For example, a 'scroll to top' button that should only appear once a LazyColumn has scrolled past item 5 depends on firstVisibleItemIndex, which changes on every pixel of scroll — but the boolean 'past item 5' only flips twice in that whole range. Wrapping the computation in remember { derivedStateOf { ... } } means the composables reading the derived boolean only recompose on those two flips, not on every scroll delta.
Cricket analogy: A ball-by-ball run counter updates every delivery, but the 'follow-on enforced' flag only flips once per innings when the run gap crosses 200 — derivedStateOf recomputes only on that rare flip, not on every single run scored.
@Composable
fun ArticleList(articles: List<Article>) {
val listState = rememberLazyListState()
val showScrollToTop by remember {
derivedStateOf { listState.firstVisibleItemIndex > 5 }
}
Box {
LazyColumn(state = listState) {
items(articles, key = { it.id }) { article ->
ArticleRow(article)
}
}
if (showScrollToTop) {
ScrollToTopFab(onClick = { /* scroll logic */ })
}
}
}LaunchedEffect and DisposableEffect
LaunchedEffect(key1, key2) { ... } launches a coroutine scoped to the composition; the block runs when the composable first enters composition and re-runs whenever any key changes, automatically cancelling the previous coroutine first. It's the standard way to trigger a one-time or key-driven suspend operation, like loading data when a screen appears or when an ID parameter changes. DisposableEffect is for non-coroutine resources that need explicit cleanup — registering a listener or receiver — and requires an onDispose { } block that Compose guarantees to call when the composable leaves the composition or the keys change.
Cricket analogy: LaunchedEffect(matchId) is like a scorer starting a fresh scorecard the moment a new match ID is announced, automatically discarding the old match's card first, while DisposableEffect is like formally handing back the stadium's PA microphone with an explicit onDispose when the broadcast ends.
@Composable
fun UserProfileScreen(userId: String, viewModel: UserProfileViewModel) {
LaunchedEffect(userId) {
viewModel.loadUser(userId)
}
val lifecycleOwner = LocalLifecycleOwner.current
DisposableEffect(lifecycleOwner) {
val observer = LifecycleEventObserver { _, event ->
if (event == Lifecycle.Event.ON_RESUME) viewModel.refresh()
}
lifecycleOwner.lifecycle.addObserver(observer)
onDispose {
lifecycleOwner.lifecycle.removeObserver(observer)
}
}
}Think of derivedStateOf as a debounce for recomposition, not for time: it collapses many rapid upstream changes into far fewer downstream notifications, but only when the derived result genuinely differs, not on a timer.
A common mistake is calling a suspend function directly inside a composable body, or worse, launching a coroutine with GlobalScope inside a composable. Both bypass Compose's lifecycle management: GlobalScope coroutines never get cancelled when the composable leaves the tree, causing leaks and crashes from updating disposed UI state. Always use LaunchedEffect or rememberCoroutineScope for coroutines tied to a composable's lifetime.
- derivedStateOf wraps a computed value so dependent composables only recompose when the derived result actually changes, not on every upstream update.
- LaunchedEffect runs a coroutine scoped to composition, restarting automatically when its keys change and cancelling the previous run.
- DisposableEffect is for effects needing explicit cleanup via onDispose, such as registering and unregistering listeners.
- SideEffect publishes Compose state to non-Compose code after every successful recomposition.
- rememberCoroutineScope gives you a coroutine scope tied to the composition for launching coroutines from event callbacks like onClick.
- Using GlobalScope or calling suspend functions directly in a composable body bypasses lifecycle safety and risks leaks or crashes.
Practice what you learned
1. What specific recomposition problem does derivedStateOf solve?
2. What happens to a running LaunchedEffect coroutine when its key parameter changes?
3. Which effect handler is designed for cleanup of non-coroutine resources like broadcast receivers or listeners?
4. Why is using GlobalScope to launch a coroutine directly inside a composable considered dangerous?
5. In the ArticleList example, why is derivedStateOf used for showScrollToTop instead of directly computing `listState.firstVisibleItemIndex > 5` inline in the composable body?
Was this page helpful?
You May Also Like
mutableStateOf and Recomposition
Dive into how mutableStateOf creates observable state that triggers recomposition, and how Compose determines which composables need to redraw.
State and remember
Understand how Compose tracks mutable values across recompositions using the remember function, and why state that isn't remembered gets lost.
Coroutines Basics in Android
Kotlin coroutines provide a lightweight, structured way to write asynchronous, non-blocking code on Android using suspend functions, coroutine builders, dispatchers, and scopes.
Kotlin Flow Fundamentals
Flow is Kotlin's asynchronous stream type for emitting multiple sequential values over time, built on coroutines, and central to reactive state management in Android apps.