LaunchedEffect and DisposableEffect
The Side Effect Problem
Composable functions can be called multiple times during recomposition. Running side effects directly in a composable body (network calls, database writes, subscriptions) would cause them to execute on every recomposition. Compose provides specific side effect APIs to handle this correctly.
LaunchedEffect
LaunchedEffect runs a coroutine scoped to a composable. It executes when the composable enters composition and cancels when it leaves. It also re-launches when its key changes.
@Composable
fun TimerScreen() {
var seconds by remember { mutableIntStateOf(0) }
// Re-launches every time `seconds` key changes
LaunchedEffect(seconds) {
delay(1000)
seconds++
}
Text(
text = "Elapsed: $seconds seconds",
style = MaterialTheme.typography.headlineMedium
))
}
If you pass Unit as the key, it runs once on composition:
@Composable
fun FetchDataScreen(userId: String) {
var user by remember { mutableStateOf<User?>(null) }
// Runs once when the composable enters composition
LaunchedEffect(userId) {
user = repository.getUser(userId)
}
user?.let { UserContent(it) } ?: CircularProgressIndicator()
}
DisposableEffect
DisposableEffect is for side effects that need cleanup (subscriptions, listeners, observers). It provides an onDispose block.
@Composable
fun LifecycleObserver() {
val lifecycleOwner = LocalLifecycleOwner.current
DisposableEffect(lifecycleOwner) {
val observer = LifecycleEventObserver { _, event ->
Log.d("Lifecycle", "Event: $event")
}
lifecycleOwner.lifecycle.addObserver(observer)
onDispose {
lifecycleOwner.lifecycle.removeObserver(observer)
}
}
}
DisposableEffect must have at least one key. It re-launches when keys change and calls onDispose before re-launching or when the composable leaves composition.
Side-by-Side Comparison
| API | Runs Coroutine | Cleanup | Re-launches on Key Change |
|---|---|---|---|
| LaunchedEffect | Yes | Automatic (cancels) | Yes |
| DisposableEffect | No (manual launch) | onDispose block | Yes |
| SideEffect | No | No | Every recomposition |
| rememberCoroutineScope | Yes (manual) | Manual | No |
SideEffect
SideEffect runs after every successful recomposition. Use it to sync Compose state with non-Compose code:
@Composable
fun AnalyticsScreen(screenName: String) {
SideEffect {
analytics.logScreenView(screenName)
}
Text("Welcome to $screenName")
}
LaunchedEffect for Navigation Events
A common pattern: trigger one-time events from ViewModel:
@Composable
fun ScreenWithEvents(viewModel: MyViewModel = hiltViewModel()) {
val events by viewModel.events.collectAsStateWithLifecycle()
events.firstOrNull()?.let { event ->
LaunchedEffect(event) {
when (event) {
is ShowSnackbar -> snackbarHostState.showSnackbar(event.message)
is Navigate -> navController.navigate(event.route)
}
viewModel.eventConsumed(event)
}
}
}
rememberCoroutineScope and Advanced Patterns
rememberCoroutineScope
rememberCoroutineScope provides a coroutine scope tied to the composition. Unlike LaunchedEffect, it does not automatically launch a coroutine. You launch manually in response to events.
@Composable
fun ScrollToTopScreen() {
val scope = rememberCoroutineScope()
val listState = rememberLazyListState()
Scaffold(
floatingActionButton = {
FloatingActionButton(onClick = {
scope.launch {
listState.animateScrollToItem(0)
}
}) {
Icon(Icons.Default.ArrowUpward, contentDescription = "Scroll to top")
}
}
) { padding ->
LazyColumn(state = listState, modifier = Modifier.padding(padding)) {
items(100) { Text("Item $it", modifier = Modifier.padding(16.dp)) }
}
}
}
The scope cancels when the composable leaves composition, preventing leaks.
When to Use Which
Use LaunchedEffect when:
- You need to run a coroutine that starts automatically.
- The effect is tied to specific state values (as keys).
- You need automatic cancellation.
Use rememberCoroutineScope when:
- You need to launch coroutines in response to user events (clicks, gestures).
- You need multiple launches from the same scope.
- The coroutine start time is not tied to composition.
Use DisposableEffect when:
- You need to set up and tear down resources (listeners, observers).
- You need an
onDisposecleanup callback.
produceState
produceState creates a composable-scoped coroutine that produces a State value:
@Composable
fun LoadImage(url: String): State<ImageBitmap?> {
return produceState<ImageBitmap?>(initialValue = null, url) {
value = withContext(Dispatchers.IO) {
url.toUri().toImageBitmap(context)
}
}
}
@Composable
fun ImageScreen(url: String) {
val image by LoadImage(url)
image?.let {
Image(bitmap = it, contentDescription = null)
} ?: CircularProgressIndicator()
}
This bridges coroutines and state management cleanly.
derivedStateOf + LaunchedEffect Pattern
Combine derivedStateOf with LaunchedEffect for scroll-based triggers:
@Composable
fun PaginatedList(items: List<Item>) {
val listState = rememberLazyListState()
// Detect when user scrolls near the end
val shouldLoadMore by remember {
derivedStateOf {
val lastVisible = listState.layoutInfo.visibleItemsInfo.lastOrNull()?.index ?: 0
lastVisible >= items.size - 5
}
}
LaunchedEffect(shouldLoadMore) {
if (shouldLoadMore) {
loadNextPage()
}
}
LazyColumn(state = listState) {
items(items, key = { it.id }) { item ->
ItemRow(item)
}
}
}
This avoids polling and only triggers pagination when the user actually scrolls near the end.
Quiz
1. What happens when you pass a new key to LaunchedEffect?
2. When should you use DisposableEffect instead of LaunchedEffect?
3. What does rememberCoroutineScope provide?
4. What is the purpose of produceState?
Flashcards
Question
What is the difference between LaunchedEffect and DisposableEffect?
Click to reveal answer
Answer
LaunchedEffect runs a coroutine and auto-cancels. DisposableEffect provides onDispose for explicit cleanup of non-coroutine resources like listeners and observers.
Question
When should you use rememberCoroutineScope vs LaunchedEffect?
Click to reveal answer
Answer
rememberCoroutineScope for event-driven launches (button clicks, gestures). LaunchedEffect for automatic coroutine launches tied to keys or composition.
Question
What does SideEffect do in Compose?
Click to reveal answer
Answer
Runs after every successful recomposition. Use it to sync Compose state with non-Compose code like analytics or logging.
Question
What is produceState used for?
Click to reveal answer
Answer
Creates a composable-scoped coroutine that produces a State value. Bridges coroutines and Compose state for loading async data.
Revision Notes
Key Takeaways
- 1. LaunchedEffect runs coroutines tied to composition lifecycle with automatic cancellation
- 2. DisposableEffect provides onDispose for cleaning up listeners, observers, and subscriptions
- 3. rememberCoroutineScope is for event-driven coroutine launches, not automatic ones
- 4. produceState bridges coroutines and Compose state for async data loading
- 5. SideEffect syncs Compose state with non-Compose code after every recomposition
Interview Tips
- • Explain when to use LaunchedEffect vs DisposableEffect with concrete examples
- • Discuss the coroutine lifecycle in Compose: when coroutines start, cancel, and re-launch
- • Know how to trigger one-time navigation events from a ViewModel using LaunchedEffect
- • Be ready to design a pagination system using derivedStateOf and LaunchedEffect
Cheat Sheet
Side Effects Cheat Sheet
LaunchedEffect(key):
- Runs coroutine on composition, re-launches on key change
- Auto-cancels when composable leaves composition
- Use for: data loading, timers, one-time events
DisposableEffect(key):
- Provides onDispose cleanup block
- Use for: listeners, observers, subscriptions
- Must have at least one key
rememberCoroutineScope:
- Manual coroutine launching in event handlers
- Cancels on composition disposal
- Use for: scroll animations, click-triggered work
SideEffect:
- Runs after every recomposition
- Use for: analytics, logging, non-Compose sync
produceState(initialValue, keys):
- Produces a State from a coroutine
- Use for: async data loading into state
Pattern: derivedStateOf + LaunchedEffect:
- Detect scroll position, trigger pagination
- Avoids polling, event-driven