rememberCoroutineScope
Every side effect we've covered so far — LaunchedEffect, DisposableEffect,
produceState — is something you write directly in the body of a composable function.
But a lot of coroutine work is triggered by a one-time user action, like tapping a button, and an
onClick lambda is not composable context — you cannot call
LaunchedEffect from inside it, because LaunchedEffect only makes sense as
part of composition itself, not as a response to an event. rememberCoroutineScope() is
the bridge for exactly this situation: it hands you a CoroutineScope you can freely
launch coroutines from, anywhere, including from inside a click handler.
Why LaunchedEffect Doesn't Work Here
LaunchedEffect starts its coroutine as part of composition and re-evaluates its key on
every recomposition — it's designed to answer "what coroutine should be running given the current
state," not "run this once, right now, because the user just clicked something." Trying to call it
from inside Button(onClick = { LaunchedEffect(...) }) won't even compile:
onClick is a plain lambda, not a @Composable function.
rememberCoroutineScope(): A Scope Tied to the Composition
Calling rememberCoroutineScope() in a composable's body gives you back a
CoroutineScope that survives recomposition (thanks to remember) and is
automatically cancelled when the composable leaves composition — the same lifecycle guarantee
LaunchedEffect gives you, just packaged as a scope you keep around and call
.launch { } on whenever you need to, rather than something Compose runs for you
automatically.
@Composable
fun SomeScreen() {
val scope = rememberCoroutineScope()
Button(onClick = {
// Launch a coroutine from a plain click handler —
// not possible with LaunchedEffect, which isn't callable here
scope.launch {
doSomeSuspendWork()
}
}) {
Text("Do it")
}
}
Worked Example: Showing a Snackbar From a Button Click
Showing a Material Snackbar is itself a suspend call — SnackbarHostState.showSnackbar()
suspends until the snackbar is dismissed or times out. That makes it a perfect fit for
rememberCoroutineScope: RecipeBox's Add/Edit Recipe screen wants to confirm a save with a
Snackbar the instant the user taps "Save," directly from that button's onClick.
@Composable
fun AddEditRecipeScreen(viewModel: AddEditRecipeViewModel, recipe: Recipe) {
val snackbarHostState = remember { SnackbarHostState() }
val scope = rememberCoroutineScope()
Scaffold(
snackbarHost = { SnackbarHost(snackbarHostState) }
) { padding ->
Column(modifier = Modifier.padding(padding)) {
// ...title, ingredients, steps, category, prepMinutes fields
Button(onClick = {
viewModel.saveRecipe(recipe)
// showSnackbar() is a suspend function — we need a scope to call it
// from this plain onClick lambda, so we launch it on our remembered scope
scope.launch {
snackbarHostState.showSnackbar("Recipe saved")
}
}) {
Text("Save Recipe")
}
}
}
}
If the user navigates away from this screen right after tapping Save, the scope returned by
rememberCoroutineScope() is cancelled along with the rest of the composition — so the
in-flight Snackbar coroutine is cleaned up automatically, exactly the same safety net
LaunchedEffect and DisposableEffect give you, just triggered manually
instead of running as part of composition itself.