Jetpack Compose has revolutionized Android development with its declarative UI approach. With Material 3 now integrated, developers can create visually appealing and highly interactive components, such as BottomSheets, with ease. In this post, we’ll dive deep into creating a sleek BottomSheet in Jetpack Compose using Material 3, focusing on best practices, customization, and advanced use cases to level up your UI game.
What is a BottomSheet?
A BottomSheet is a widely used UI component that slides up from the bottom of the screen to present additional content or actions without disrupting the current screen. In Jetpack Compose, BottomSheets can be categorized into:
Modal BottomSheet: Blocks interaction with the rest of the screen until dismissed.
Persistent BottomSheet: Remains on screen alongside other content, often used for supplementary information or controls.
Material 3 enhances BottomSheets with a fresh design language, adaptive theming, and improved accessibility, making them indispensable in modern app design.
Setting Up the Environment
Before diving in, ensure your project is set up with the latest Jetpack Compose and Material 3 libraries:
implementation("androidx.compose.material3:material3:1.2.0") // Replace with the latest version
implementation("androidx.compose.ui:ui:1.5.0") // Replace with the latest version
Also, enable Jetpack Compose in your project-level build.gradle
:
buildFeatures {
compose true
}
composeOptions {
kotlinCompilerExtensionVersion '1.5.0'
}
Building a Basic Modal BottomSheet
Let’s start with a simple implementation of a Modal BottomSheet using Material 3’s ModalBottomSheet
API. Below is an example:
@Composable
fun SimpleModalBottomSheet() {
val sheetState = rememberModalBottomSheetState(
initialValue = ModalBottomSheetValue.Hidden
)
val scope = rememberCoroutineScope()
ModalBottomSheetLayout(
sheetState = sheetState,
sheetContent = {
Column(
modifier = Modifier
.fillMaxWidth()
.padding(16.dp),
horizontalAlignment = Alignment.CenterHorizontally
) {
Text("This is a Modal BottomSheet", style = MaterialTheme.typography.titleMedium)
Spacer(modifier = Modifier.height(8.dp))
Button(onClick = { scope.launch { sheetState.hide() } }) {
Text("Close")
}
}
}
) {
Button(onClick = { scope.launch { sheetState.show() } }) {
Text("Show BottomSheet")
}
}
}
Key Points:
rememberModalBottomSheetState
: Manages the state of the BottomSheet (e.g.,Hidden
,Expanded
,HalfExpanded
).ModalBottomSheetLayout
: Provides the container for the BottomSheet and the main content.CoroutineScope
: Used to trigger state changes like showing or hiding the BottomSheet.
Advanced Customization and Styling
Material 3 allows extensive customization of BottomSheets to align them with your app’s design system. Let’s explore some advanced styling options:
1. Customizing Shape and Elevation
@Composable
fun StyledModalBottomSheet() {
val sheetState = rememberModalBottomSheetState(ModalBottomSheetValue.Hidden)
val scope = rememberCoroutineScope()
ModalBottomSheetLayout(
sheetState = sheetState,
sheetShape = RoundedCornerShape(topStart = 16.dp, topEnd = 16.dp),
sheetElevation = 8.dp,
sheetContent = {
Column(
modifier = Modifier
.fillMaxWidth()
.padding(16.dp),
horizontalAlignment = Alignment.CenterHorizontally
) {
Text("Styled BottomSheet", style = MaterialTheme.typography.titleMedium)
Spacer(modifier = Modifier.height(8.dp))
Button(onClick = { scope.launch { sheetState.hide() } }) {
Text("Close")
}
}
}
) {
Button(onClick = { scope.launch { sheetState.show() } }) {
Text("Show Styled BottomSheet")
}
}
}
Highlights:
sheetShape
: Controls the corner radius of the BottomSheet.sheetElevation
: Adjusts the shadow depth for a layered effect.
2. Adding a Drag Handle
Drag handles improve usability by signaling that the BottomSheet is draggable:
@Composable
fun DragHandleBottomSheet() {
val sheetState = rememberModalBottomSheetState(ModalBottomSheetValue.Hidden)
val scope = rememberCoroutineScope()
ModalBottomSheetLayout(
sheetState = sheetState,
sheetContent = {
Column(
modifier = Modifier
.fillMaxWidth()
.padding(16.dp),
horizontalAlignment = Alignment.CenterHorizontally
) {
Box(
modifier = Modifier
.width(40.dp)
.height(4.dp)
.clip(RoundedCornerShape(2.dp))
.background(Color.Gray)
.align(Alignment.CenterHorizontally)
)
Spacer(modifier = Modifier.height(8.dp))
Text("BottomSheet with Drag Handle", style = MaterialTheme.typography.titleMedium)
Spacer(modifier = Modifier.height(8.dp))
Button(onClick = { scope.launch { sheetState.hide() } }) {
Text("Close")
}
}
}
) {
Button(onClick = { scope.launch { sheetState.show() } }) {
Text("Show BottomSheet")
}
}
}
Key Additions:
Drag handle implemented with a simple
Box
and customizable properties.Enhanced user experience by visually indicating interactivity.
Persistent BottomSheet Example
For scenarios where a BottomSheet coexists with other content, Persistent BottomSheets come into play. Here’s an example:
@Composable
fun PersistentBottomSheet() {
val scaffoldState = rememberBottomSheetScaffoldState(
bottomSheetState = BottomSheetState(BottomSheetValue.Collapsed)
)
val scope = rememberCoroutineScope()
BottomSheetScaffold(
scaffoldState = scaffoldState,
sheetContent = {
Column(
modifier = Modifier
.fillMaxWidth()
.padding(16.dp),
horizontalAlignment = Alignment.CenterHorizontally
) {
Text("Persistent BottomSheet", style = MaterialTheme.typography.titleMedium)
Spacer(modifier = Modifier.height(8.dp))
Button(onClick = { scope.launch { scaffoldState.bottomSheetState.collapse() } }) {
Text("Collapse")
}
}
},
sheetPeekHeight = 64.dp
) {
Column(
modifier = Modifier.fillMaxSize(),
horizontalAlignment = Alignment.CenterHorizontally,
verticalArrangement = Arrangement.Center
) {
Button(onClick = { scope.launch { scaffoldState.bottomSheetState.expand() } }) {
Text("Expand BottomSheet")
}
}
}
}
Features:
BottomSheetScaffold
: Combines BottomSheet with a Scaffold layout.sheetPeekHeight
: Defines how much of the BottomSheet is visible when collapsed.BottomSheetState
: Manages the expanded and collapsed states.
Best Practices for BottomSheets in Jetpack Compose
Accessibility: Ensure all interactive elements are labeled and focusable.
Adaptive Design: Use
WindowSizeClass
to adapt the BottomSheet behavior for tablets and foldables.Performance: Avoid heavy recomposition by using
remember
and state management best practices.Testing: Test BottomSheet states and gestures using Compose’s built-in testing framework.
Conclusion
Jetpack Compose and Material 3 make it easier than ever to build sleek, interactive BottomSheets that enhance the user experience. Whether you’re creating modal or persistent sheets, the customization options and best practices outlined here will help you design functional and visually stunning components.
Start implementing BottomSheets today to take your app’s UI to the next level! If you’ve found this guide helpful, share it with your peers and bookmark it for future reference.