Skip to Content

Bottom Sheet

Hướng dẫn sử dụng Bottom Sheet trong Compose Multiplatform.

@OptIn(ExperimentalMaterial3Api::class) @Composable fun ModalBottomSheetExample() { var showBottomSheet by remember { mutableStateOf(false) } val sheetState = rememberModalBottomSheetState() Scaffold { padding -> Box(modifier = Modifier.padding(padding)) { Button(onClick = { showBottomSheet = true }) { Text("Show Bottom Sheet") } } } if (showBottomSheet) { ModalBottomSheet( onDismissRequest = { showBottomSheet = false }, sheetState = sheetState ) { BottomSheetContent( onDismiss = { showBottomSheet = false } ) } } } @Composable fun BottomSheetContent(onDismiss: () -> Unit) { Column( modifier = Modifier .fillMaxWidth() .padding(16.dp) ) { Text( "Bottom Sheet", style = MaterialTheme.typography.titleLarge, fontWeight = FontWeight.Bold ) Spacer(Modifier.height(16.dp)) Text("This is the content of the bottom sheet.") Spacer(Modifier.height(24.dp)) Button( onClick = onDismiss, modifier = Modifier.fillMaxWidth() ) { Text("Close") } Spacer(Modifier.height(32.dp)) // Padding cho navigation bar } }

Bottom Sheet với List

@OptIn(ExperimentalMaterial3Api::class) @Composable fun OptionsBottomSheet( show: Boolean, onDismiss: () -> Unit, onOptionSelected: (String) -> Unit ) { if (show) { ModalBottomSheet( onDismissRequest = onDismiss, sheetState = rememberModalBottomSheetState() ) { Column(modifier = Modifier.padding(bottom = 32.dp)) { Text( "Select an option", style = MaterialTheme.typography.titleMedium, modifier = Modifier.padding(16.dp) ) listOf( "Option 1" to Icons.Default.Star, "Option 2" to Icons.Default.Favorite, "Option 3" to Icons.Default.Share, "Cancel" to Icons.Default.Close ).forEach { (option, icon) -> ListItem( headlineContent = { Text(option) }, leadingContent = { Icon(icon, contentDescription = null) }, modifier = Modifier.clickable { onOptionSelected(option) onDismiss() } ) } } } } }

Share Bottom Sheet

@OptIn(ExperimentalMaterial3Api::class) @Composable fun ShareBottomSheet( show: Boolean, onDismiss: () -> Unit, onShare: (ShareOption) -> Unit ) { if (show) { ModalBottomSheet( onDismissRequest = onDismiss, sheetState = rememberModalBottomSheetState() ) { Column(modifier = Modifier.padding(16.dp)) { Text( "Share via", style = MaterialTheme.typography.titleMedium, fontWeight = FontWeight.Bold ) Spacer(Modifier.height(16.dp)) Row( modifier = Modifier.fillMaxWidth(), horizontalArrangement = Arrangement.SpaceEvenly ) { ShareOption.entries.forEach { option -> ShareOptionItem( option = option, onClick = { onShare(option) onDismiss() } ) } } Spacer(Modifier.height(32.dp)) } } } } enum class ShareOption(val label: String, val icon: ImageVector) { COPY("Copy", Icons.Default.ContentCopy), MESSAGE("Message", Icons.Default.Message), EMAIL("Email", Icons.Default.Email), MORE("More", Icons.Default.MoreHoriz) } @Composable fun ShareOptionItem( option: ShareOption, onClick: () -> Unit ) { Column( horizontalAlignment = Alignment.CenterHorizontally, modifier = Modifier .clickable(onClick = onClick) .padding(8.dp) ) { Box( modifier = Modifier .size(56.dp) .background( MaterialTheme.colorScheme.primaryContainer, CircleShape ), contentAlignment = Alignment.Center ) { Icon( option.icon, contentDescription = option.label, tint = MaterialTheme.colorScheme.onPrimaryContainer ) } Spacer(Modifier.height(8.dp)) Text( option.label, style = MaterialTheme.typography.bodySmall ) } }

Filter Bottom Sheet

@OptIn(ExperimentalMaterial3Api::class) @Composable fun FilterBottomSheet( show: Boolean, currentFilter: FilterState, onDismiss: () -> Unit, onApply: (FilterState) -> Unit ) { if (show) { var filter by remember { mutableStateOf(currentFilter) } ModalBottomSheet( onDismissRequest = onDismiss, sheetState = rememberModalBottomSheetState(skipPartiallyExpanded = true) ) { Column( modifier = Modifier .fillMaxWidth() .padding(16.dp) ) { // Header Row( modifier = Modifier.fillMaxWidth(), horizontalArrangement = Arrangement.SpaceBetween, verticalAlignment = Alignment.CenterVertically ) { Text( "Filters", style = MaterialTheme.typography.titleLarge, fontWeight = FontWeight.Bold ) TextButton(onClick = { filter = FilterState() }) { Text("Reset") } } Spacer(Modifier.height(24.dp)) // Sort by Text( "Sort by", style = MaterialTheme.typography.titleSmall, color = Color.Gray ) Spacer(Modifier.height(8.dp)) FlowRow( horizontalArrangement = Arrangement.spacedBy(8.dp) ) { SortOption.entries.forEach { option -> FilterChip( selected = filter.sortBy == option, onClick = { filter = filter.copy(sortBy = option) }, label = { Text(option.label) } ) } } Spacer(Modifier.height(24.dp)) // Price range Text( "Price range", style = MaterialTheme.typography.titleSmall, color = Color.Gray ) Spacer(Modifier.height(8.dp)) RangeSlider( value = filter.priceRange, onValueChange = { filter = filter.copy(priceRange = it) }, valueRange = 0f..1000f ) Row( modifier = Modifier.fillMaxWidth(), horizontalArrangement = Arrangement.SpaceBetween ) { Text("$${filter.priceRange.start.toInt()}") Text("$${filter.priceRange.endInclusive.toInt()}") } Spacer(Modifier.height(24.dp)) // Categories Text( "Categories", style = MaterialTheme.typography.titleSmall, color = Color.Gray ) Spacer(Modifier.height(8.dp)) Category.entries.forEach { category -> Row( modifier = Modifier .fillMaxWidth() .clickable { filter = filter.copy( categories = if (filter.categories.contains(category)) { filter.categories - category } else { filter.categories + category } ) } .padding(vertical = 8.dp), verticalAlignment = Alignment.CenterVertically ) { Checkbox( checked = filter.categories.contains(category), onCheckedChange = null ) Spacer(Modifier.width(8.dp)) Text(category.label) } } Spacer(Modifier.height(24.dp)) // Apply button Button( onClick = { onApply(filter) onDismiss() }, modifier = Modifier .fillMaxWidth() .height(56.dp), shape = RoundedCornerShape(12.dp) ) { Text("Apply Filters") } Spacer(Modifier.height(32.dp)) } } } } data class FilterState( val sortBy: SortOption = SortOption.NEWEST, val priceRange: ClosedFloatingPointRange<Float> = 0f..500f, val categories: Set<Category> = emptySet() ) enum class SortOption(val label: String) { NEWEST("Newest"), PRICE_LOW("Price: Low"), PRICE_HIGH("Price: High"), POPULAR("Popular") } enum class Category(val label: String) { ELECTRONICS("Electronics"), CLOTHING("Clothing"), BOOKS("Books"), HOME("Home & Garden") }

Confirmation Bottom Sheet

@OptIn(ExperimentalMaterial3Api::class) @Composable fun ConfirmationBottomSheet( show: Boolean, title: String, message: String, confirmText: String = "Confirm", cancelText: String = "Cancel", isDestructive: Boolean = false, onConfirm: () -> Unit, onDismiss: () -> Unit ) { if (show) { ModalBottomSheet( onDismissRequest = onDismiss, sheetState = rememberModalBottomSheetState() ) { Column( modifier = Modifier .fillMaxWidth() .padding(24.dp), horizontalAlignment = Alignment.CenterHorizontally ) { Icon( if (isDestructive) Icons.Default.Warning else Icons.Default.Info, contentDescription = null, modifier = Modifier.size(48.dp), tint = if (isDestructive) { MaterialTheme.colorScheme.error } else { MaterialTheme.colorScheme.primary } ) Spacer(Modifier.height(16.dp)) Text( title, style = MaterialTheme.typography.titleLarge, fontWeight = FontWeight.Bold ) Spacer(Modifier.height(8.dp)) Text( message, style = MaterialTheme.typography.bodyMedium, textAlign = TextAlign.Center, color = Color.Gray ) Spacer(Modifier.height(24.dp)) Row( modifier = Modifier.fillMaxWidth(), horizontalArrangement = Arrangement.spacedBy(16.dp) ) { OutlinedButton( onClick = onDismiss, modifier = Modifier.weight(1f), shape = RoundedCornerShape(12.dp) ) { Text(cancelText) } Button( onClick = { onConfirm() onDismiss() }, modifier = Modifier.weight(1f), shape = RoundedCornerShape(12.dp), colors = if (isDestructive) { ButtonDefaults.buttonColors( containerColor = MaterialTheme.colorScheme.error ) } else { ButtonDefaults.buttonColors() } ) { Text(confirmText) } } Spacer(Modifier.height(32.dp)) } } } }

📝 Tóm tắt

ComponentMục đích
ModalBottomSheetStandard bottom sheet
rememberModalBottomSheetStateSheet state
skipPartiallyExpandedFull expansion

Best Practices

  1. Padding bottom cho navigation bar
  2. Drag handle tự động có sẵn
  3. skipPartiallyExpanded cho form dài
  4. onDismissRequest để close khi tap outside

Tiếp theo

Học về Dialog.

Last updated on