Tools: Dialogs in Jetpack Compose: AlertDialog, BottomSheet, and Snackbar

Tools: Dialogs in Jetpack Compose: AlertDialog, BottomSheet, and Snackbar

Source: Dev.to

AlertDialog: Simple Confirmations ## Custom AlertDialog with Input ## ModalBottomSheet: Complex Interactions ## Snackbar: Brief Notifications ## Complete Delete Confirmation Pattern ## Best Practices ## Summary 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 is the most common dialog type. It interrupts the user with a modal overlay, requiring explicit action before dismissal. Call this from your screen state: The key pattern: onDismissRequest fires when the user taps outside the dialog or presses back. Always handle this to prevent stuck dialogs. For collecting text input, add a TextField: For richer interactions or large content, ModalBottomSheet slides up from the bottom. It's less intrusive than AlertDialog and supports scrolling. ModalBottomSheet automatically handles landscape orientation and provides drag-to-dismiss. Users appreciate the non-intrusive nature compared to full-screen dialogs. Snackbars appear at the bottom without blocking interaction. Use SnackbarHost with Scaffold: For actions, capture the result: Combining all three for a comprehensive delete flow: 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 onDismissRequest to 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: Master three dialog types: All 8 templates include dialog patterns. https://myougatheax.gumroad.com Templates let you quickly answer FAQs or store snippets for re-use. Are you sure you want to hide this comment? It will become hidden in your post, but will still be visible via the comment's permalink. Hide child comments as well For further actions, you may consider blocking this person and/or reporting abuse COMMAND_BLOCK: @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") } } ) } Enter fullscreen mode Exit fullscreen mode COMMAND_BLOCK: @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") } } ) } COMMAND_BLOCK: @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") } } ) } CODE_BLOCK: var showDeleteDialog by remember { mutableStateOf(false) } if (showDeleteDialog) { DeleteConfirmationDialog( onConfirm = { deleteItem() }, onDismiss = { showDeleteDialog = false } ) } Button(onClick = { showDeleteDialog = true }) { Text("Delete Item") } Enter fullscreen mode Exit fullscreen mode CODE_BLOCK: var showDeleteDialog by remember { mutableStateOf(false) } if (showDeleteDialog) { DeleteConfirmationDialog( onConfirm = { deleteItem() }, onDismiss = { showDeleteDialog = false } ) } Button(onClick = { showDeleteDialog = true }) { Text("Delete Item") } CODE_BLOCK: var showDeleteDialog by remember { mutableStateOf(false) } if (showDeleteDialog) { DeleteConfirmationDialog( onConfirm = { deleteItem() }, onDismiss = { showDeleteDialog = false } ) } Button(onClick = { showDeleteDialog = true }) { Text("Delete Item") } COMMAND_BLOCK: @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") } } ) } Enter fullscreen mode Exit fullscreen mode COMMAND_BLOCK: @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") } } ) } COMMAND_BLOCK: @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") } } ) } COMMAND_BLOCK: @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") } } } } } } Enter fullscreen mode Exit fullscreen mode COMMAND_BLOCK: @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") } } } } } } COMMAND_BLOCK: @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") } } } } } } CODE_BLOCK: var showDetailsSheet by remember { mutableStateOf(false) } if (showDetailsSheet) { ItemDetailsBottomSheet( item = selectedItem, onDismiss = { showDetailsSheet = false } ) } Enter fullscreen mode Exit fullscreen mode CODE_BLOCK: var showDetailsSheet by remember { mutableStateOf(false) } if (showDetailsSheet) { ItemDetailsBottomSheet( item = selectedItem, onDismiss = { showDetailsSheet = false } ) } CODE_BLOCK: var showDetailsSheet by remember { mutableStateOf(false) } if (showDetailsSheet) { ItemDetailsBottomSheet( item = selectedItem, onDismiss = { showDetailsSheet = false } ) } CODE_BLOCK: @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") } } } } Enter fullscreen mode Exit fullscreen mode CODE_BLOCK: @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") } } } } CODE_BLOCK: @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") } } } } COMMAND_BLOCK: 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) } } Enter fullscreen mode Exit fullscreen mode COMMAND_BLOCK: 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) } } COMMAND_BLOCK: 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) } } CODE_BLOCK: @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") } } ) } } Enter fullscreen mode Exit fullscreen mode CODE_BLOCK: @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") } } ) } } CODE_BLOCK: @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") } } ) } } CODE_BLOCK: var isDeleting by remember { mutableStateOf(false) } Button( onClick = { isDeleting = true viewModel.deleteItem(selectedItemId) { isDeleting = false } }, enabled = !isDeleting ) { Text(if (isDeleting) "Deleting..." else "Delete") } Enter fullscreen mode Exit fullscreen mode CODE_BLOCK: var isDeleting by remember { mutableStateOf(false) } Button( onClick = { isDeleting = true viewModel.deleteItem(selectedItemId) { isDeleting = false } }, enabled = !isDeleting ) { Text(if (isDeleting) "Deleting..." else "Delete") } CODE_BLOCK: var isDeleting by remember { mutableStateOf(false) } Button( onClick = { isDeleting = true viewModel.deleteItem(selectedItemId) { isDeleting = false } }, enabled = !isDeleting ) { Text(if (isDeleting) "Deleting..." else "Delete") } - 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 onDismissRequest to 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: - AlertDialog: Confirmations and simple input - ModalBottomSheet: Rich content and complex flows - Snackbar + SnackbarHost: Non-blocking feedback and undo