Exploring Jetpack Compose Bottom Sheet (with Examples)

Jetpack Compose Bottom Sheet

In this article, we will explore bottom sheet APIs in Android Jetpack Compose. We will discuss both the modal and persistent bottom sheets with examples.

Prerequisites:

What is the Bottom Sheet in Android?

It is a UI component anchored to the bottom of the screen. It enters and leaves the screen with a nice animation.

Jetpack Compose provides two types of bottom sheet APIs:

  1. Modal Bottom Sheet
  2. Persistent Bottom Sheet

1. Modal Bottom Sheet:

It is an alternative to dialogs or menus. Users should dismiss the sheet to interact with the app.

Example:

Jetpack Compose Modal Bottom Sheet

Let’s implement it. Jetpack Compose provides ModalBottomSheetLayout(). Here is how it looks:

@Composable
@ExperimentalMaterialApi
fun ModalBottomSheetLayout(
    sheetContent: @Composable ColumnScope.() -> Unit,
    modifier: Modifier = Modifier,
    sheetState: ModalBottomSheetState =
        rememberModalBottomSheetState(Hidden),
    sheetShape: Shape = MaterialTheme.shapes.large,
    sheetElevation: Dp = ModalBottomSheetDefaults.Elevation,
    sheetBackgroundColor: Color = MaterialTheme.colors.surface,
    sheetContentColor: Color = contentColorFor(sheetBackgroundColor),
    scrimColor: Color = ModalBottomSheetDefaults.scrimColor,
    content: @Composable () -> Unit
)

sheetContent – The content of the bottom sheet.

modifier – To modify the layout.

sheetStateState of the sheet that is used to show or hide it.

sheetShapeThe shape of the sheet.

sheetElevation – The shadow behind the sheet.

sheetBackgroundColor – The background color.

sheetContentColor – The preferred color for the content.

scrimColor – The overlay color that is applied to the rest of the screen while the bottom sheet is visible. If you pass Color.Unspecified, the scrim will no longer be applied and the bottom sheet will not block interaction with the rest of the screen.

content – The content of the rest of the screen.

First, we need a state object to manage the sheet state. We can get it from rememberModalBottomSheetState().

@Composable
@ExperimentalMaterialApi
fun rememberModalBottomSheetState(
    initialValue: ModalBottomSheetValue,
    animationSpec: AnimationSpec<Float> = SwipeableDefaults.AnimationSpec,
    confirmStateChange: (ModalBottomSheetValue) -> Boolean = { true }
): ModalBottomSheetState

initialValue – The initial value of the state.

animationSpecThe animation used while the state changes.

confirmStateChange – Optional callback invoked to confirm or veto a pending state change.

For this article, create a MyUI() composable outside the MainActivity class.

@OptIn(ExperimentalMaterialApi::class)
@Composable
fun MyUI() {
    
    val bottomSheetState =
        rememberModalBottomSheetState(initialValue = ModalBottomSheetValue.Hidden)

}

Now, we can call hide() and show() methods on the bottomSheetState object. But these are suspend functions. They should be called from a coroutine. In Jetpack Compose, we can get the coroutine scope from the rememberCoroutineScope() composable.

We will also display a toast when we tap on the item. Toast requires context . We can obtain it from LocalContext.current.

@OptIn(ExperimentalMaterialApi::class)
@Composable
fun MyUI() {
    val contextForToast = LocalContext.current.applicationContext

    val bottomSheetState =
        rememberModalBottomSheetState(initialValue = ModalBottomSheetValue.Hidden)

    val coroutineScope = rememberCoroutineScope()

}

We will display 5 items using the LazyColum API. You may need to add Material Icon Dependency for the favorite icon.

@OptIn(ExperimentalMaterialApi::class)
@Composable
fun MyUI() {

    val contextForToast = LocalContext.current.applicationContext

    val bottomSheetState =
        rememberModalBottomSheetState(initialValue = ModalBottomSheetValue.Hidden)

    val coroutineScope = rememberCoroutineScope()

    ModalBottomSheetLayout(
        sheetContent = {

            LazyColumn {

                items(count = 5) {

                    ListItem(
                        modifier = Modifier.clickable {
                            
                            Toast.makeText(contextForToast, "Item $it", Toast.LENGTH_SHORT)
                                .show()

                            coroutineScope.launch {
                                bottomSheetState.hide()
                            }
                        },
                        text = {
                            Text(text = "Item $it")
                        },
                        icon = {
                            Icon(
                                imageVector = Icons.Default.Favorite,
                                contentDescription = "Favorite"
                            )
                        }
                    )
                }
            }
        },
        sheetState = bottomSheetState
    ) {
        // app UI
        Column(
            modifier = Modifier.fillMaxSize(),
            verticalArrangement = Arrangement.Center,
            horizontalAlignment = Alignment.CenterHorizontally
        ) {
            Button(onClick = {
                coroutineScope.launch {
                    bottomSheetState.show()
                }
            }) {
                Text(text = "Show Bottom Sheet")
            }
        }
    }
}

Call it in the onCreate() method like this:

class MainActivity : ComponentActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContent {
            YourProjectNameTheme(darkTheme = false) {
                Surface(
                    modifier = Modifier.fillMaxSize(),
                    color = MaterialTheme.colors.background
                ) {
                    Column(
                        modifier = Modifier.fillMaxSize()
                    ) {
                        MyUI()
                    }
                }
            }
        }
    }
}

Output:

Jetpack Compose Modal Bottom Sheet

2. Persistent Bottom Sheet (with Scaffold):

It stays at the bottom of the screen. Users can interact with the app while it is shown. They can swipe up to expand the sheet.

It has two states – collapsed and expanded. In the collapsed state, it remains at the bottom. In the expanded state, it shows the additional content.

Example:

Sheet Collapsed
Collapsed State
Sheet Expanded
Expanded State

We can implement it with the BottomSheetScaffold() composable.

@Composable
@ExperimentalMaterialApi
fun BottomSheetScaffold(
    sheetContent: @Composable ColumnScope.() -> Unit,
    modifier: Modifier = Modifier,
    scaffoldState: BottomSheetScaffoldState = rememberBottomSheetScaffoldState(),
    topBar: (@Composable () -> Unit)? = null,
    snackbarHost: @Composable (SnackbarHostState) -> Unit = { SnackbarHost(it) },
    floatingActionButton: (@Composable () -> Unit)? = null,
    floatingActionButtonPosition: FabPosition = FabPosition.End,
    sheetGesturesEnabled: Boolean = true,
    sheetShape: Shape = MaterialTheme.shapes.large,
    sheetElevation: Dp = BottomSheetScaffoldDefaults.SheetElevation,
    sheetBackgroundColor: Color = MaterialTheme.colors.surface,
    sheetContentColor: Color = contentColorFor(sheetBackgroundColor),
    sheetPeekHeight: Dp = BottomSheetScaffoldDefaults.SheetPeekHeight,
    drawerContent: @Composable (ColumnScope.() -> Unit)? = null,
    drawerGesturesEnabled: Boolean = true,
    drawerShape: Shape = MaterialTheme.shapes.large,
    drawerElevation: Dp = DrawerDefaults.Elevation,
    drawerBackgroundColor: Color = MaterialTheme.colors.surface,
    drawerContentColor: Color = contentColorFor(drawerBackgroundColor),
    drawerScrimColor: Color = DrawerDefaults.scrimColor,
    backgroundColor: Color = MaterialTheme.colors.background,
    contentColor: Color = contentColorFor(backgroundColor),
    content: @Composable (PaddingValues) -> Unit
)

Don’t get scared of the parameters. As it is a Scaffold, it accepts TopAppBar, FAB, Bottom Sheet, and Navigation Drawer. Let’s look at the parameters related to the bottom sheet.

sheetGesturesEnabled – Whether the bottom sheet can be interacted with by gestures.

sheetShapeThe shape of the bottom sheet.

sheetElevation – The shadow behind the bottom sheet.

sheetBackgroundColor – The background color.

sheetContentColor – The preferred color for the content.

sheetPeekHeight – The height of the bottom sheet when it is collapsed.

To manage the state, use rememberBottomSheetScaffoldState().

@Composable
@ExperimentalMaterialApi
fun rememberBottomSheetScaffoldState(
    drawerState: DrawerState = rememberDrawerState(DrawerValue.Closed),
    bottomSheetState: BottomSheetState = rememberBottomSheetState(BottomSheetValue.Collapsed),
    snackbarHostState: SnackbarHostState = remember { SnackbarHostState() }
): BottomSheetScaffoldState

drawerState, bottomSheetState, and snackbarHostState represent the states of the respective components.

val scaffoldState = rememberBottomSheetScaffoldState()

Now, we can call collapse() and expand() methods like this:

scaffoldState.bottomSheetState.collapse() – To collapse the sheet.

scaffoldState.bottomSheetState.expand() – To expand the sheet.

These are suspend functions. So, we need the coroutine scope.

@OptIn(ExperimentalMaterialApi::class)
@Composable
fun MyUI() {
    val contextForToast = LocalContext.current.applicationContext
    val coroutineScope = rememberCoroutineScope()
    val scaffoldState = rememberBottomSheetScaffoldState()
}

We will use the same LazyColumn to display the bottom sheet content.

@OptIn(ExperimentalMaterialApi::class)
@Composable
fun MyUI() {

    val contextForToast = LocalContext.current.applicationContext

    val coroutineScope = rememberCoroutineScope()

    val scaffoldState = rememberBottomSheetScaffoldState()

    BottomSheetScaffold(
        scaffoldState = scaffoldState,
        sheetPeekHeight = 56.dp,
        sheetContent = {

            LazyColumn {

                // the first item that is visible
                item {
                    Box(
                        modifier = Modifier
                            .height(56.dp)
                            .fillMaxWidth()
                            .background(color = MaterialTheme.colors.primary)
                    ) {
                        Text(
                            text = "Swipe up to Expand the sheet",
                            modifier = Modifier.align(alignment = Alignment.Center),
                            color = Color.White
                        )
                    }
                }

                // remaining items
                items(count = 5) {
                    ListItem(
                        modifier = Modifier.clickable {

                            Toast.makeText(contextForToast, "Item $it", Toast.LENGTH_SHORT)
                                .show()

                            coroutineScope.launch {
                                scaffoldState.bottomSheetState.collapse()
                                scaffoldState.bottomSheetState.expand()
                            }
                        },
                        text = {
                            Text(text = "Item $it")
                        },
                        icon = {
                            Icon(
                                imageVector = Icons.Default.Favorite,
                                contentDescription = "Favorite",
                                tint = MaterialTheme.colors.primary
                            )
                        }
                    )
                }
            }
        },
        floatingActionButton = {
            FloatingActionButton(onClick = {
                Toast.makeText(contextForToast, "FAB Click", Toast.LENGTH_SHORT).show()
            }) {
                Icon(
                    imageVector = Icons.Default.Navigation,
                    contentDescription = "Favorite"
                )
            }
        }) {
        // app UI
        Column(
            modifier = Modifier.fillMaxSize(),
            verticalArrangement = Arrangement.Center,
            horizontalAlignment = Alignment.CenterHorizontally
        ) {
            Text(text = "Rest of the app UI")
        }
    }
}

Run it and you will see the following output:

Sheet Collapsed
Collapsed State
Sheet Expanded
Expanded State

Note: You can also put the bottom sheet content in a separate composable to make the code more readable.

Continue Exploring Jetpack Compose:

References:

Leave a Comment