DEV Community

myougaTheAxo
myougaTheAxo

Posted on

Dialogs in Jetpack Compose: AlertDialog, BottomSheet, and Snackbar

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")
            }
        }
    )
}
Enter fullscreen mode Exit fullscreen mode

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")
}
Enter fullscreen mode Exit fullscreen mode

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")
            }
        }
    )
}
Enter fullscreen mode Exit fullscreen mode

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")
                    }
                }
            }
        }
    }
}
Enter fullscreen mode Exit fullscreen mode

Usage:

var showDetailsSheet by remember { mutableStateOf(false) }

if (showDetailsSheet) {
    ItemDetailsBottomSheet(
        item = selectedItem,
        onDismiss = { showDetailsSheet = false }
    )
}
Enter fullscreen mode Exit fullscreen mode

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")
            }
        }
    }
}
Enter fullscreen mode Exit fullscreen mode

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)
    }
}
Enter fullscreen mode Exit fullscreen mode

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")
                }
            }
        )
    }
}
Enter fullscreen mode Exit fullscreen mode

Best Practices

  1. Modal vs Non-Modal: Use AlertDialog when the user must respond. Use BottomSheet for exploratory actions. Use Snackbar for non-critical feedback.

  2. Dismiss Handling: Always implement onDismissRequest to handle back button and outside taps gracefully.

  3. State Management: Keep dialog state simple. Use ViewModel if managing complex confirmation flows.

  4. Accessibility: Ensure buttons have meaningful labels. Test with screen readers.

  5. Undo Patterns: Always offer undo for destructive actions via Snackbar, but make redo within a short time window (5 seconds).

  6. 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")
}
Enter fullscreen mode Exit fullscreen mode

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)