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:
| Parameter | Purpose |
|---|---|
onDismissRequest | Called on outside-tap or back press — you decide whether that closes it |
title | Short heading composable, usually just a Text |
text | The body message composable |
confirmButton | Usually a TextButton for the affirmative action |
dismissButton | Optional 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.