Dialogs are essential UI components for capturing user attention and collecting input. Jetpack Compose provides multiple dialog types: AlertDialog for simple confirmations, ModalBottomSheet for complex interactions, and Snackbar for brief notifications. This guide covers all three with practical examples.
AlertDialog: Simple Confirmations
AlertDialog is the most common dialog type. It interrupts the user with a modal overlay, requiring explicit action before dismissal.
@Composable
fun DeleteConfirmationDialog(
onConfirm: () -> Unit,
onDismiss: () -> Unit
) {
AlertDialog(
onDismissRequest = onDismiss,
title = { Text("Delete Item") },
text = { Text("Are you sure you want to delete this item? This action cannot be undone.") },
confirmButton = {
Button(
onClick = {
onConfirm()
onDismiss()
}
) {
Text("Delete")
}
},
dismissButton = {
Button(onClick = onDismiss) {
Text("Cancel")
}
}
)
}
Call this from your screen state:
var showDeleteDialog by remember { mutableStateOf(false) }
if (showDeleteDialog) {
DeleteConfirmationDialog(
onConfirm = { deleteItem() },
onDismiss = { showDeleteDialog = false }
)
}
Button(onClick = { showDeleteDialog = true }) {
Text("Delete Item")
}
The key pattern: onDismissRequest fires when the user taps outside the dialog or presses back. Always handle this to prevent stuck dialogs.
Custom AlertDialog with Input
For collecting text input, add a TextField:
@Composable
fun RenameDialog(
currentName: String,
onConfirm: (String) -> Unit,
onDismiss: () -> Unit
) {
var newName by remember { mutableStateOf(currentName) }
AlertDialog(
onDismissRequest = onDismiss,
title = { Text("Rename") },
text = {
TextField(
value = newName,
onValueChange = { newName = it },
label = { Text("New name") },
modifier = Modifier.fillMaxWidth()
)
},
confirmButton = {
Button(
onClick = {
onConfirm(newName)
onDismiss()
}
) {
Text("Save")
}
},
dismissButton = {
Button(onClick = onDismiss) {
Text("Cancel")
}
}
)
}
ModalBottomSheet: Complex Interactions
For richer interactions or large content, ModalBottomSheet slides up from the bottom. It's less intrusive than AlertDialog and supports scrolling.
@Composable
fun ItemDetailsBottomSheet(
item: Item,
onDismiss: () -> Unit
) {
val sheetState = rememberModalBottomSheetState()
ModalBottomSheet(
onDismissRequest = onDismiss,
sheetState = sheetState
) {
LazyColumn(
modifier = Modifier
.fillMaxWidth()
.padding(16.dp)
) {
item {
Text(
text = item.name,
style = MaterialTheme.typography.headlineSmall,
modifier = Modifier.padding(bottom = 16.dp)
)
}
item {
Text(
text = item.description,
style = MaterialTheme.typography.bodyMedium,
modifier = Modifier.padding(bottom = 24.dp)
)
}
item {
Row(
modifier = Modifier
.fillMaxWidth()
.padding(bottom = 16.dp),
horizontalArrangement = Arrangement.SpaceBetween
) {
Button(
onClick = { /* Edit */ }
) {
Text("Edit")
}
Button(
onClick = { /* Delete */ }
) {
Text("Delete")
}
}
}
}
}
}
Usage:
var showDetailsSheet by remember { mutableStateOf(false) }
if (showDetailsSheet) {
ItemDetailsBottomSheet(
item = selectedItem,
onDismiss = { showDetailsSheet = false }
)
}
ModalBottomSheet automatically handles landscape orientation and provides drag-to-dismiss. Users appreciate the non-intrusive nature compared to full-screen dialogs.
Snackbar: Brief Notifications
Snackbars appear at the bottom without blocking interaction. Use SnackbarHost with Scaffold:
@Composable
fun MainScreen() {
val snackbarHostState = remember { SnackbarHostState() }
Scaffold(
snackbarHost = { SnackbarHost(snackbarHostState) }
) { paddingValues ->
Column(
modifier = Modifier
.fillMaxSize()
.padding(paddingValues)
) {
Button(
onClick = {
coroutineScope.launch {
snackbarHostState.showSnackbar(
message = "Item deleted",
actionLabel = "Undo",
duration = SnackbarDuration.Short
)
}
}
) {
Text("Delete")
}
}
}
}
For actions, capture the result:
val result = snackbarHostState.showSnackbar(
message = "Item deleted",
actionLabel = "Undo",
duration = SnackbarDuration.Short
)
when (result) {
SnackbarResult.ActionPerformed -> {
// User tapped "Undo"
restoreItem()
}
SnackbarResult.Dismissed -> {
// Snackbar dismissed (timeout or swipe)
}
}
Complete Delete Confirmation Pattern
Combining all three for a comprehensive delete flow:
@Composable
fun ItemListWithDelete() {
val snackbarHostState = remember { SnackbarHostState() }
var showDeleteDialog by remember { mutableStateOf(false) }
var selectedItemId by remember { mutableStateOf<Int?>(null) }
Scaffold(
snackbarHost = { SnackbarHost(snackbarHostState) }
) { paddingValues ->
Column(
modifier = Modifier
.fillMaxSize()
.padding(paddingValues)
) {
// List of items with delete button
LazyColumn {
items(items) { item ->
Row(
modifier = Modifier
.fillMaxWidth()
.padding(16.dp)
) {
Text(item.name, modifier = Modifier.weight(1f))
Button(
onClick = {
selectedItemId = item.id
showDeleteDialog = true
}
) {
Text("Delete")
}
}
}
}
}
}
if (showDeleteDialog) {
AlertDialog(
onDismissRequest = { showDeleteDialog = false },
title = { Text("Delete Item") },
text = { Text("Are you sure? This cannot be undone.") },
confirmButton = {
Button(
onClick = {
deleteItem(selectedItemId!!)
showDeleteDialog = false
// Show confirmation
coroutineScope.launch {
val result = snackbarHostState.showSnackbar(
message = "Item deleted",
actionLabel = "Undo",
duration = SnackbarDuration.Short
)
if (result == SnackbarResult.ActionPerformed) {
restoreItem(selectedItemId!!)
}
}
}
) {
Text("Delete")
}
},
dismissButton = {
Button(onClick = { showDeleteDialog = false }) {
Text("Cancel")
}
}
)
}
}
Best Practices
Modal vs Non-Modal: Use AlertDialog when the user must respond. Use BottomSheet for exploratory actions. Use Snackbar for non-critical feedback.
Dismiss Handling: Always implement
onDismissRequestto handle back button and outside taps gracefully.State Management: Keep dialog state simple. Use ViewModel if managing complex confirmation flows.
Accessibility: Ensure buttons have meaningful labels. Test with screen readers.
Undo Patterns: Always offer undo for destructive actions via Snackbar, but make redo within a short time window (5 seconds).
Loading States: Disable buttons during async operations to prevent duplicate submissions:
var isDeleting by remember { mutableStateOf(false) }
Button(
onClick = {
isDeleting = true
viewModel.deleteItem(selectedItemId) { isDeleting = false }
},
enabled = !isDeleting
) {
Text(if (isDeleting) "Deleting..." else "Delete")
}
Summary
Master three dialog types:
- AlertDialog: Confirmations and simple input
- ModalBottomSheet: Rich content and complex flows
- Snackbar + SnackbarHost: Non-blocking feedback and undo
All 8 templates include dialog patterns. https://myougatheax.gumroad.com
Top comments (0)