๐ŸŽจ Jetpack Compose

Side Effects in Compose: LaunchedEffect, SideEffect, and DisposableEffect

Composables should describe UI, not do work. But real screens must show snackbars, start animations, register listeners and log analytics. Compose's effect APIs let you run that work at the right moment, with automatic cleanup. Here's which one to use, and how keys control them.

โœ๏ธ Yunok Tech ๐Ÿ“… Updated โฑ 3 min read ๐ŸŽฏ Intermediate
On this page
  1. Which effect API?
  2. LaunchedEffect
  3. rememberCoroutineScope: effects from callbacks
  4. DisposableEffect: register and clean up
  5. rememberUpdatedState: avoid stale lambdas
  6. SideEffect and produceState
  7. Real-world use cases
  8. Common mistakes
  9. Frequently asked questions
  10. Conclusion

A side effect is any change to the world outside a composable's own UI: a network call, an analytics event, a registered listener, a snackbar. Because composables may run many times, in any order, or be skipped, side effects in their bodies run unpredictably. The effect APIs give you control.

Which effect API?

APIRunsCleanupUse for
LaunchedEffect(keys)A coroutine on entering composition; restarts when keys changeCoroutine cancelled on leave or restartSuspend work tied to the composable: snackbars, animations, collecting events
rememberCoroutineScope()When you launch from a callbackScope cancelled on leaveCoroutines started by user events (onClick โ†’ scroll)
DisposableEffect(keys)On enter; re-runs when keys changeonDispose { } requiredRegister/unregister listeners, observers, callbacks
SideEffectAfter every successful recompositionNonePublishing Compose state to non-Compose objects
rememberUpdatedState--Read the latest lambda or value inside a long-running effect
produceStateCoroutine producing a StateCancelled on leaveConverting non-Compose sources into State
snapshotFlow--Turning Compose state reads into a Flow
Lifecycle of LaunchedEffect with keys When the composable enters composition, LaunchedEffect starts a coroutine. If its key changes, the coroutine is cancelled and a new one starts. When the composable leaves composition, the coroutine is cancelled. Enterstart coroutine Recomposesame key โ†’ keep Key changescancel โ†’ restart Leavecancel coroutine Keys = "restart this effect when these values change". LaunchedEffect(Unit) runs once per entry into composition.
Choosing keys is the most important decision for every effect.

LaunchedEffect

Show a snackbar for a message in stateKotlin
@Composable
fun NotesScreen(state: NotesUiState, onMessageShown: () -> Unit) {
    val snackbarHostState = remember { SnackbarHostState() }

    state.userMessage?.let { message ->
        LaunchedEffect(message) {                 // restarts only for a new message
            snackbarHostState.showSnackbar(message)
            onMessageShown()
        }
    }
    Scaffold(snackbarHost = { SnackbarHost(snackbarHostState) }) { padding -> /* ... */ }
}

rememberCoroutineScope: effects from callbacks

Scroll to top on clickKotlin
val listState = rememberLazyListState()
val scope = rememberCoroutineScope()

FloatingActionButton(onClick = {
    scope.launch { listState.animateScrollToItem(0) }   // launched by an event, not by composition
}) { Icon(Icons.Default.KeyboardArrowUp, contentDescription = "Scroll to top") }

You can't call LaunchedEffect inside onClick, because callbacks aren't composable. Use the remembered scope instead.

DisposableEffect: register and clean up

Observe lifecycle eventsKotlin
@Composable
fun TrackScreenTime(screenName: String, onTimeSpent: (String, Long) -> Unit) {
    val lifecycleOwner = LocalLifecycleOwner.current
    val currentOnTimeSpent by rememberUpdatedState(onTimeSpent)   // always call the latest lambda

    DisposableEffect(lifecycleOwner, screenName) {
        var resumedAt = 0L
        val observer = LifecycleEventObserver { _, event ->
            when (event) {
                Lifecycle.Event.ON_RESUME -> resumedAt = SystemClock.elapsedRealtime()
                Lifecycle.Event.ON_PAUSE -> currentOnTimeSpent(screenName, SystemClock.elapsedRealtime() - resumedAt)
                else -> Unit
            }
        }
        lifecycleOwner.lifecycle.addObserver(observer)
        onDispose { lifecycleOwner.lifecycle.removeObserver(observer) }   // always paired
    }
}

Lifecycle-runtime-compose also offers LifecycleResumeEffect and LifecycleStartEffect for these common cases.

rememberUpdatedState: avoid stale lambdas

Splash timeoutKotlin
@Composable
fun SplashTimeout(onTimeout: () -> Unit) {
    val latestOnTimeout by rememberUpdatedState(onTimeout)
    LaunchedEffect(Unit) {                 // don't restart the timer when the lambda changes...
        delay(2_000)
        latestOnTimeout()                  // ...but still call the newest one
    }
}

SideEffect and produceState

SideEffect: sync Compose state outwardKotlin
SideEffect { analytics.setUserProperty("theme", if (isDark) "dark" else "light") }
produceState: non-Compose source โ†’ StateKotlin
@Composable
fun rememberBatteryLevel(context: Context): State<Int?> = produceState<Int?>(initialValue = null, context) {
    val receiver = object : BroadcastReceiver() {
        override fun onReceive(c: Context, i: Intent) { value = i.getIntExtra(BatteryManager.EXTRA_LEVEL, -1) }
    }
    ContextCompat.registerReceiver(context, receiver, IntentFilter(Intent.ACTION_BATTERY_CHANGED), ContextCompat.RECEIVER_NOT_EXPORTED)
    awaitDispose { context.unregisterReceiver(receiver) }
}

Real-world use cases

Load data for an ID

LaunchedEffect(id) { viewModel.load(id) }, or better, let the ViewModel load from SavedStateHandle in init.

Location or sensor updates

DisposableEffect, or lifecycle effects, to start on resume and stop on pause.

One-time onboarding hint

LaunchedEffect(Unit) to show it, plus persistence so it doesn't show again.

Common mistakes

  • Wrong keys: LaunchedEffect(Unit) when the effect depends on a changing ID, or keys that change every recomposition (new objects), which restarts constantly.
  • Forgetting onDispose cleanup, which leaks listeners.
  • Business logic in effects that belongs in the ViewModel.
  • Writing state in SideEffect, triggering endless recompositions.
  • Collecting flows in LaunchedEffect without lifecycle awareness. Use collectAsStateWithLifecycle or repeatOnLifecycle.

Frequently asked questions

Is LaunchedEffect(Unit) like onCreate?

Roughly, but per composition entry, not per Activity. It runs again if the composable leaves and re-enters composition.

Do effects survive configuration changes?

No. The Activity is recreated, so composition restarts and effects run again. Keep long-running work in the ViewModel.

Can I pass multiple keys?

Yes, for example LaunchedEffect(userId, filter). The effect restarts when any key changes.

Conclusion

Keep composable bodies pure and move work into the right effect: LaunchedEffect for suspend work tied to composition, rememberCoroutineScope for event-driven coroutines, DisposableEffect for register and cleanup, SideEffect for publishing state outward. Choose keys deliberately.

Next step: make components reusable and testable with State Hoisting.

Official references

๐Ÿ“ฑ Apps built by Yunok Tech

Real Android apps on Google Play, free to download.

See all our apps โ†’

Ready to build real Android applications?

Jetpack Compose clicks once you build real screens with real state. Practise modern Android UI development on projects that go beyond toy examples.

Move beyond theory and strengthen your Android development skills with Yunoktech's Android course. Learn core concepts, practical implementation, modern Android development techniques, and the reasoning behind building maintainable applications.

Explore the Android course and take your next step as an Android developer.

Share: LinkedIn X / Twitter WhatsApp