Dialogs & AlertDialog

Deleting a recipe is destructive and hard to undo mentally — the user should have to confirm it first. In the View system this meant building an AlertDialog.Builder chain imperatively; in Compose, a dialog is just another composable, shown or hidden the exact same way any other UI is: by reading a piece of state. This lesson adds a "Delete recipe?" confirmation to the RecipeBox recipe list from the previous lesson.

AlertDialog's Anatomy

Material 3's AlertDialog composable takes an onDismissRequest callback (fired when the user taps outside the dialog or presses back) plus slots for its content:

ParameterPurpose
onDismissRequestCalled on outside-tap or back press — you decide whether that closes it
titleShort heading composable, usually just a Text
textThe body message composable
confirmButtonUsually a TextButton for the affirmative action
dismissButtonOptional TextButton for "Cancel"

Controlling Visibility With Boolean State

An AlertDialog is only composed onto the screen while a condition is true — there is no separate "show()"/"dismiss()" API to call. The standard pattern is a single mutableStateOf(false) flag:

var showDeleteDialog by remember { mutableStateOf(false) }
var recipeToDelete by remember { mutableStateOf<Recipe?>(null) }

// From a delete icon's onClick:
recipeToDelete = recipe
showDeleteDialog = true

Worked Example: "Delete recipe?" Confirmation

The dialog itself is only composed if (showDeleteDialog) is true, and every button inside it is responsible for flipping that flag back off:

if (showDeleteDialog && recipeToDelete != null) {
    AlertDialog(
        onDismissRequest = { showDeleteDialog = false },
        title = { Text("Delete recipe?") },
        text = {
            Text("\"${recipeToDelete?.title}\" will be permanently removed. This can't be undone.")
        },
        confirmButton = {
            TextButton(onClick = {
                onDeleteConfirmed(recipeToDelete!!)
                showDeleteDialog = false
            }) {
                Text("Delete")
            }
        },
        dismissButton = {
            TextButton(onClick = { showDeleteDialog = false }) {
                Text("Cancel")
            }
        }
    )
}

Because the dialog is placed inside RecipeListScreen itself (alongside the Scaffold from the previous lesson, not inside it), it draws as a full-screen overlay above everything else regardless of where in the composable tree the trigger button lives.

A Note on onDismissRequest

It's tempting to leave onDismissRequest empty, but doing so means the dialog can never be closed by tapping outside it or pressing the system back button — Android will still deliver both of those events to the composable, and ignoring them traps the user. Always set showDeleteDialog = false there, exactly as the buttons do.

Beyond AlertDialog: The Plain Dialog Composable

When a confirmation-style title/text/two-buttons layout doesn't fit — say, a fully custom card with an embedded form — Compose exposes the lower-level Dialog { } composable, which gives you an empty floating window and lets you place any content inside it yourself:

Dialog(onDismissRequest = { showCustom = false }) {
    Surface(shape = RoundedCornerShape(16.dp)) {
        // any composables at all — this is a blank canvas
    }
}

Reach for AlertDialog first — it handles Material styling and button layout for you — and drop to plain Dialog only when the content truly doesn't fit that shape.

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: Snackbar shows a brief, non-blocking "Recipe saved!" confirmation instead of an interruptive dialog — useful for actions that don't need a yes/no answer.