Choosing the Right Side Effect
We've now covered five APIs that all exist to solve the same underlying problem — letting a purely declarative composable reach out and touch the non-Compose world (coroutines, listeners, analytics SDKs, callback-based APIs) safely, without leaking work past the lifetime of the screen that started it. The hard part in practice isn't understanding any one of them in isolation, it's recognizing which one a given situation calls for. This lesson is a single reference table plus a set of RecipeBox scenarios to test that instinct.
The Decision Table
| API | Use this when... |
|---|---|
LaunchedEffect(key) | you need to run suspend work tied to a composable's presence on screen, and want it to automatically re-run whenever a specific value (the key) changes. |
DisposableEffect(key) | you're registering something non-Compose (a listener, a receiver, an observer) that has an explicit "undo" step — onDispose { } guarantees that cleanup runs. |
SideEffect { } | you need to publish the latest Compose state out to a non-Compose object on every successful recomposition — no key, no cleanup, just "keep this in sync." |
produceState(...) | you have a callback-based or otherwise non-Compose async source and want its latest value exposed as ordinary Compose State<T> that any composable can read. |
rememberCoroutineScope() | you need to launch a coroutine from a non-composable context — a button's onClick, or any other event lambda — where you can't call LaunchedEffect directly. |
A Quick Gut Check
One useful way to sort these: are you reacting to composition itself (a screen
appearing, a value changing), or to a discrete event (a click)? The first four all
run automatically as Compose recomposes; only rememberCoroutineScope hands you something
to call on demand from outside that automatic flow.
RecipeBox Scenarios: Pick the Right Tool
| Scenario | Right choice |
|---|---|
| RecipeBox's recipe list needs to load its data from the repository exactly once, the first time the list screen appears. | LaunchedEffect(Unit) |
| The search screen needs to re-query recipes every time the user's search text changes, cancelling any still-running query for a stale term. | LaunchedEffect(searchQuery) |
| The Add/Edit Recipe screen registers a location listener while visible, and must unregister it the moment the user navigates away. | DisposableEffect(key) { onDispose { ... } } |
Tapping "Save Recipe" needs to trigger both a repository save call and a Snackbar confirmation, from inside the button's onClick. |
rememberCoroutineScope(), then scope.launch { } |
An analytics SDK's currentScreen property needs to always reflect whichever RecipeBox screen just finished recomposing. |
SideEffect { } |
A legacy, callback-based geocoding library needs its latest result exposed as Compose State so a composable can read it with by. |
produceState(...) |
LaunchedEffect/rememberCoroutineScope) or callback/listener-based (leans DisposableEffect/produceState), then whether it's triggered by composition or by a discrete event.