100% Free Forever
AI-Powered Learning
Industry Expert Content
Certificates & Badges
Learn At Your Own Pace
Kotlin

derivedStateOf and Side Effects

Learn how derivedStateOf avoids unnecessary recomposition for computed values, and how effect handlers like LaunchedEffect manage side effects safely.

State ManagementAdvanced11 min readJul 8, 2026
Analogies

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.

kotlin
@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.

kotlin
@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

Was this page helpful?

Topics covered

#Kotlin#AndroidWithJetpackComposeStudyNotes#MobileDevelopment#DerivedStateOfAndSideEffects#DerivedStateOf#Side#Effects#Computed#StudyNotes#SkillVeris