Snackbar

Not every confirmation deserves an AlertDialog blocking the whole screen — telling the user "Recipe saved!" after they tap Save is exactly the kind of low-stakes, transient feedback a Snackbar is built for: it appears briefly near the bottom of the screen, doesn't demand a response, and disappears on its own. Because showing one is an asynchronous, timed event rather than a simple state flip, it's also our first real look at triggering UI from a coroutine.

SnackbarHostState & SnackbarHost

A snackbar has two halves: SnackbarHostState, which holds the queue of messages waiting to be shown, and SnackbarHost, the composable that actually renders whatever is currently at the front of that queue. SnackbarHost is designed to slot directly into Scaffold's snackbarHost parameter:

val snackbarHostState = remember { SnackbarHostState() }
val scope = rememberCoroutineScope()

Scaffold(
    snackbarHost = { SnackbarHost(hostState = snackbarHostState) }
) { innerPadding ->
    // screen content
}

rememberCoroutineScope() gives us a CoroutineScope tied to this composable's lifecycle — we'll cover it properly in the Side Effects part of this track, but for now: it's what lets a plain button click, which isn't itself a suspend function, launch a suspending call like showSnackbar.

Showing a Snackbar From a Coroutine

SnackbarHostState.showSnackbar(...) is a suspend function — it enqueues the message and suspends until the snackbar is dismissed (by timeout, swipe, or the action button), which is why it must run inside a coroutine launched via scope.launch:

Button(onClick = {
    saveRecipe(recipe)
    scope.launch {
        snackbarHostState.showSnackbar(message = "Recipe saved!")
    }
}) {
    Text("Save")
}

Worked Example: "Recipe saved!" With an Undo Action

showSnackbar returns a SnackbarResult once the snackbar is dismissed, telling you whether the user tapped its action button (ActionPerformed) or it just timed out / was swiped away (Dismissed). That return value is how RecipeBox offers an "Undo" after deleting a recipe:

fun onDeleteRecipe(recipe: Recipe) {
    recipes.remove(recipe)
    scope.launch {
        val result = snackbarHostState.showSnackbar(
            message = "\"${recipe.title}\" deleted",
            actionLabel = "Undo",
            duration = SnackbarDuration.Short
        )
        if (result == SnackbarResult.ActionPerformed) {
            recipes.add(recipe)
        }
    }
}

Because recipes.remove(recipe) happens immediately and the coroutine only decides whether to add it back afterward, the deletion feels instant to the user while still remaining reversible for the few seconds the snackbar is visible.

Only One Snackbar at a Time

SnackbarHostState is intentionally a single-slot queue — if showSnackbar is called again while one is already showing, the new call suspends until the current snackbar finishes, then shows next. You never need to manually dismiss a previous snackbar before showing another; just call showSnackbar again and let the host state serialize them.

Note: This is correct, real Compose/Kotlin code — but this website's own project doesn't run a live Android Studio build, so you won't see it actually render on this page.
Next up: Bottom Sheet covers a third way to surface auxiliary UI — sliding up from the bottom edge rather than floating in the middle or the corner.