Skip to content
advanced Phase 4 · Jetpack Compose

Side Effects

Handle side effects with LaunchedEffect, rememberCoroutineScope, DisposableEffect, and derivedStateOf.

50m
2 problems
Topic Progress 0%

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 onDispose cleanup 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?

Question 1 options

2. When should you use DisposableEffect instead of LaunchedEffect?

Question 2 options

3. What does rememberCoroutineScope provide?

Question 3 options

4. What is the purpose of produceState?

Question 4 options

Flashcards

Question

What is the difference between LaunchedEffect and DisposableEffect?

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?

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?

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?

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