DevBits
DevBitsAndroid Handbook
Jetpack Compose

What are side effects in Compose and how should they be handled?

Understand why real UI work needs controlled lifetimes in Compose, then choose the appropriate effect only after ownership and boundaries are clear.

CMP-009Advanced~45 sec

What the interviewer is testing

The interviewer is usually testing whether the candidate understands the boundary between describing the UI and making something happen because of the UI.

That distinction becomes important in Compose because rendering is not a one-time call. A composable can run again, leave the Composition, and later return. Work such as showing a snackbar or registering a listener cannot be treated as though the composable body runs once from top to bottom.

A strong answer therefore starts with the problem, not the API names. It should show that the candidate can reason about ownership and lifetime: which layer should perform the work, what starts it, when it should happen again, and when it should stop.

❌ Common mistake

If they ask you to elaborate

Compose is designed so that a composable can be called many times. That's what makes recomposition work.

The problem is that not everything an Android screen does can safely happen every time a function is called.

Imagine opening a screen that loads an order from the server. If that request happened on every recomposition, you'd quickly end up doing duplicated work. The same applies to showing a Snackbar, starting a camera preview, registering a BroadcastReceiver, or sending analytics events. None of those should happen simply because Compose decided to redraw part of the UI.

This is why Compose introduced effect APIs. They let work happen outside the normal rendering process while still giving that work a lifetime that follows the UI.

Before choosing an effect API, experienced developers usually answer three questions:

  • What starts this work?
  • How long should it live?
  • What should stop it?

Once those answers are clear, the appropriate API is usually obvious.

That's the mental model to keep in mind while reading the examples in the next section.

Production thinking

Experienced Android developers rarely start by choosing an effect API.

They usually start with a simpler question:

Why does this work belong to the screen?

Imagine this code appears in a pull request:

LaunchedEffect(Unit) {
    repository.loadOrders()
}

The first discussion usually isn't about LaunchedEffect. It's about ownership.

Loading orders creates application state. Other screens may need the same data, retries may be required, and the work should survive UI recreation. Those responsibilities normally belong in the ViewModel, not in the screen. Changing the effect API wouldn't fix that architectural mistake.

Now imagine you're reviewing something different:

LaunchedEffect(uiState.errorMessage) {
    uiState.errorMessage?.let {
        snackbarHostState.showSnackbar(it)
        onMessageShown()
    }
}

The first question I would ask in a code review isn't "Why did you choose LaunchedEffect?"

I'd ask "Why does showing a Snackbar belong to this screen?"

In this case it does. A Snackbar is purely UI behaviour. It isn't application state, another screen doesn't care about it, and once the user leaves the screen there's no reason for that work to continue.

Once we've agreed on ownership, LaunchedEffect becomes an obvious choice. The message appearing starts the work, and if the screen leaves the composition the coroutine is cancelled automatically. Under the hood that's exactly what LaunchedEffect gives you: a coroutine whose lifetime follows the UI.

Notice the final line:

onMessageShown()

LaunchedEffect doesn't solve one-off events. It only runs the coroutine. The ViewModel still needs to know that the message has been handled; otherwise the same message may be shown again after the screen is recreated.

Now consider a button that scrolls a list back to the top:

val scope = rememberCoroutineScope()

Button(onClick = {
    scope.launch {
        listState.animateScrollToItem(0)
    }
}) { Text("Top") }

Here the work already has a natural trigger: the user clicked the button. Creating extra state just to trigger a LaunchedEffect would make the code harder to understand because the click itself already tells us when the work should start.

rememberCoroutineScope fits because the coroutine belongs to that user interaction. If the composable disappears, the scope is cancelled as well.

Another example is a barcode scanner.

Starting the scanner is easy. The difficult part is remembering to stop it. If the user navigates away while the scanner is still running, you've leaked work that no longer belongs to the screen.

DisposableEffect(scanner) {
    scanner.start()

    onDispose {
        scanner.stop()
    }
}

DisposableEffect exists for exactly this situation. It gives Compose a place to pair setup with cleanup. Choosing another effect here usually means remembering to clean everything up manually, and that's exactly the kind of bug that appears months later.

Analytics is a different kind of work.

Nothing is happening because the user clicked something. We're simply telling another system what the UI currently looks like.

SideEffect {
    analytics.setCurrentScreen("Orders")
}

SideEffect runs after Compose has successfully updated the UI, so the analytics system always receives the latest values. Sending analytics before recomposition completes could report stale information instead.

Finally, imagine a timeout:

val currentOnTimeout by rememberUpdatedState(onTimeout)

LaunchedEffect(Unit) {
    delay(2_000)
    currentOnTimeout()
}

The timeout should always fire after two seconds.

The callback, however, might change while those two seconds are passing. Restarting the timer every time the callback changes would be the real bug because the user could end up waiting forever.

rememberUpdatedState solves that specific problem. It keeps the latest callback available without restarting work that's already in progress.

Experienced engineers usually ask the same questions before reaching for any effect API:

  • Who owns this work?
  • Why does it belong here?
  • When should it start?
  • When should it run again?
  • When should it stop?
  • What bug would another approach introduce?

Once those questions have clear answers, the appropriate Compose effect API is usually obvious.

Follow-up questions

What separates a senior answer

Key takeaways

Learn more

On this page