Skip to main content

Build a Sleek BottomSheet in Jetpack Compose with Material 3

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:

  1. Modal BottomSheet: Blocks interaction with the rest of the screen until dismissed.

  2. 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

  1. Accessibility: Ensure all interactive elements are labeled and focusable.

  2. Adaptive Design: Use WindowSizeClass to adapt the BottomSheet behavior for tablets and foldables.

  3. Performance: Avoid heavy recomposition by using remember and state management best practices.

  4. 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.

Popular posts from this blog

Restricting Jetpack Compose TextField to Numeric Input Only

Jetpack Compose has revolutionized Android development with its declarative approach, enabling developers to build modern, responsive UIs more efficiently. Among the many components provided by Compose, TextField is a critical building block for user input. However, ensuring that a TextField accepts only numeric input can pose challenges, especially when considering edge cases like empty fields, invalid characters, or localization nuances. In this blog post, we'll explore how to restrict a Jetpack Compose TextField to numeric input only, discussing both basic and advanced implementations. Why Restricting Input Matters Restricting user input to numeric values is a common requirement in apps dealing with forms, payment entries, age verifications, or any data where only numbers are valid. Properly validating input at the UI level enhances user experience, reduces backend validation overhead, and minimizes errors during data processing. Compose provides the flexibility to implement ...

jetpack compose - TextField remove underline

Compose TextField Remove Underline The TextField is the text input widget of android jetpack compose library. TextField is an equivalent widget of the android view system’s EditText widget. TextField is used to enter and modify text. The following jetpack compose tutorial will demonstrate to us how we can remove (actually hide) the underline from a TextField widget in an android application. We have to apply a simple trick to remove (hide) the underline from the TextField. The TextField constructor’s ‘colors’ argument allows us to set or change colors for TextField’s various components such as text color, cursor color, label color, error color, background color, focused and unfocused indicator color, etc. Jetpack developers can pass a TextFieldDefaults.textFieldColors() function with arguments value for the TextField ‘colors’ argument. There are many arguments for this ‘TextFieldDefaults.textFieldColors()’function such as textColor, disabledTextColor, backgroundColor, cursorC...

jetpack compose - Image clickable

Compose Image Clickable The Image widget allows android developers to display an image object to the app user interface using the jetpack compose library. Android app developers can show image objects to the Image widget from various sources such as painter resources, vector resources, bitmap, etc. Image is a very essential component of the jetpack compose library. Android app developers can change many properties of an Image widget by its modifiers such as size, shape, etc. We also can specify the Image object scaling algorithm, content description, etc. But how can we set a click event to an Image widget in a jetpack compose application? There is no built-in property/parameter/argument to set up an onClick event directly to the Image widget. This android application development tutorial will demonstrate to us how we can add a click event to the Image widget and make it clickable. Click event of a widget allow app users to execute a task such as showing a toast message by cli...