Bottom Sheet APIs in Material 3 Jetpack Compose

Jetpack Compose Material 3 Bottom Sheet

In this article, we’ll learn how to implement the bottom sheet APIs in Material 3 Jetpack Compose.

Prerequisites:

What is a 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.

Example:

Bottom Sheet Example

There are two types of bottom sheet APIs in Material 3 Jetpack Compose:

  1. Bottom Sheet Scaffold
  2. Modal Bottom Sheet

Let’s look at each one.

First, create an empty Compose project and open MainActivity.kt. Create a MyUI() composable and call it from the onCreate() method. We’ll write our code in it.

import android.os.Bundle
import android.widget.Toast
import androidx.activity.ComponentActivity
import androidx.activity.compose.setContent
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.WindowInsets
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.width
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.selection.toggleable
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Favorite
import androidx.compose.material3.BottomSheetDefaults
import androidx.compose.material3.BottomSheetScaffold
import androidx.compose.material3.Button
import androidx.compose.material3.Checkbox
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.Icon
import androidx.compose.material3.ListItem
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.ModalBottomSheet
import androidx.compose.material3.OutlinedTextField
import androidx.compose.material3.SheetState
import androidx.compose.material3.SheetValue
import androidx.compose.material3.Surface
import androidx.compose.material3.Text
import androidx.compose.material3.rememberBottomSheetScaffoldState
import androidx.compose.material3.rememberModalBottomSheetState
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.runtime.saveable.rememberSaveable
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.semantics.Role
import androidx.compose.ui.unit.dp
import kotlinx.coroutines.launch

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

@Composable
fun MyUI() {

}

1. Bottom Sheet Scaffold:

This is the standard bottom sheet. It floats over the main content of a screen, providing additional information or functionality without blocking the user’s workflow.

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:

BottomSheetScaffold Collapsed State
Collapsed State
BottomSheetScaffold Expanded State
Expanded State

Jetpack Compose provides BottomSheetScaffold() method:

@Composable
@ExperimentalMaterial3Api
fun BottomSheetScaffold(
    sheetContent: @Composable ColumnScope.() -> Unit,
    modifier: Modifier = Modifier,
    scaffoldState: BottomSheetScaffoldState = rememberBottomSheetScaffoldState(),
    sheetPeekHeight: Dp = BottomSheetDefaults.SheetPeekHeight,
    sheetShape: Shape = BottomSheetDefaults.ExpandedShape,
    sheetContainerColor: Color = BottomSheetDefaults.ContainerColor,
    sheetContentColor: Color = contentColorFor(sheetContainerColor),
    sheetTonalElevation: Dp = BottomSheetDefaults.Elevation,
    sheetShadowElevation: Dp = BottomSheetDefaults.Elevation,
    sheetDragHandle: @Composable (() -> Unit)? = { BottomSheetDefaults.DragHandle() },
    sheetSwipeEnabled: Boolean = true,
    topBar: @Composable (() -> Unit)? = null,
    snackbarHost: @Composable (SnackbarHostState) -> Unit = { SnackbarHost(it) },
    containerColor: Color = MaterialTheme.colorScheme.surface,
    contentColor: Color = contentColorFor(containerColor),
    content: @Composable (PaddingValues) -> Unit
)

sheetContent – The content of the bottom sheet.

modifier – The Modifier to be applied to this scaffold.

scaffoldState – To manage the state of the sheet.

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

sheetShape – The shape of the sheet.

sheetContainerColor – The background color of the sheet.

sheetContentColor – The preferred color for the content.

sheetTonalElevation – The tonal elevation of the sheet.

sheetShadowElevation – The shadow elevation of the sheet.

sheetDragHandle – An optional visual marker to pull the scaffold’s bottom sheet.

sheetSwipeEnabled – Whether the swiping is enabled.

topBar – The top app bar of the screen, typically a SmallTopAppBar.

snackbarHost – The component to host the Snackbar.

containerColor – The background color of this scaffold. Use Color.Transparent to have no color.

contentColor – The preferred color for content inside this scaffold.

content – It is a lambda where we add the content of the screen. It receives PaddingValues that should be applied to the content root via Modifier.padding and Modifier.consumeWindowInsets to properly offset top and bottom bars. If you are using Modifier.verticalScroll, apply this modifier to the child of the scroll, and not to the scroll itself.

Here is a simple example:

@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun MyUI() {
    val coroutineScope = rememberCoroutineScope()
    val scaffoldState = rememberBottomSheetScaffoldState()

    BottomSheetScaffold(
        scaffoldState = scaffoldState,
        sheetPeekHeight = 128.dp, // height of the bottom sheet in the collapsed state
        sheetContent = {
            // this is the column scope
            // if you add multiple elements, they are placed vertically

            // sheet content in the collapsed state
            Box(
                Modifier
                    .fillMaxWidth()
                    .height(128.dp),
                contentAlignment = Alignment.Center
            ) {
                Text("Swipe up to expand sheet")
            }

            // sheet content in the expanded state
            Column(
                Modifier
                    .fillMaxWidth()
                    .padding(64.dp),
                horizontalAlignment = Alignment.CenterHorizontally
            ) {
                Text("Sheet content")

                Button(
                    onClick = {
                        coroutineScope.launch {
                            scaffoldState.bottomSheetState.partialExpand()
                        }
                    }
                ) {
                    Text(text = "Click to collapse sheet")
                }
            }
        }
    ) { innerPadding ->
        // add the rest of the app UI here
        Box(
            Modifier
                .padding(innerPadding)
                .fillMaxSize()
        ) {
            Text(
                modifier = Modifier.align(alignment = Alignment.Center),
                text = "Rest of the app UI"
            )
        }
    }
}

Output:

BottomSheetScaffold Collapsed State
Collapsed State
BottomSheetScaffold Expanded State
Expanded State

The SheetState provides the following suspend methods:

scaffoldState.bottomSheetState.partialExpand() – It partially expands (collapses) the sheet.

scaffoldState.bottomSheetState.expand() – It expands the sheet.

scaffoldState.bottomSheetState.hide() – It completely hides the sheet.

If you want to use the hide() method, you should set the skipPartiallyExpanded false. In the above code, change the scaffoldState parameter.

val scaffoldState = rememberBottomSheetScaffoldState(
    bottomSheetState = SheetState(
        skipPartiallyExpanded = false, // pass false here
        initialValue = SheetValue.Expanded
    )
)

As the above methods are suspend functions, they should be called from the coroutine scope.

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

Example:

ModalBottomSheet

The ModalBottomSheet() composable looks like this:

@Composable
@ExperimentalMaterial3Api
fun ModalBottomSheet(
    onDismissRequest: () -> Unit,
    modifier: Modifier = Modifier,
    sheetState: SheetState = rememberModalBottomSheetState(),
    shape: Shape = BottomSheetDefaults.ExpandedShape,
    containerColor: Color = BottomSheetDefaults.ContainerColor,
    contentColor: Color = contentColorFor(containerColor),
    tonalElevation: Dp = BottomSheetDefaults.Elevation,
    scrimColor: Color = BottomSheetDefaults.ScrimColor,
    dragHandle: @Composable (() -> Unit)? = { BottomSheetDefaults.DragHandle() },
    windowInsets: WindowInsets = BottomSheetDefaults.windowInsets,
    content: @Composable ColumnScope.() -> Unit,
)

onDismissRequest – It is called when the user clicks outside of the bottom sheet, after the sheet animates to Hidden.

modifier – Optional Modifier for the bottom sheet.

sheetState – To manage the state.

shape – The shape of the bottom sheet.

containerColor – The background color.

contentColor – The preferred color for the content.

tonalElevation – The tonal elevation of this bottom sheet.

scrimColor – The translucent overlay color that appears over the app UI when the bottom sheet is open.

dragHandle – Optional visual marker to swipe the bottom sheet.

windowInsets – Window insets to be passed to the bottom sheet via PaddingValues params.

content – The content of the sheet.

We can get the state object from the rememberModalBottomSheetState() method:

@Composable
@ExperimentalMaterial3Api
fun rememberModalBottomSheetState(
    skipPartiallyExpanded: Boolean = false,
    confirmValueChange: (SheetValue) -> Boolean = { true },
)

skipPartiallyExpanded – If true, the sheet will always expand to the Expanded state and move to the Hidden state when hiding the sheet, either programmatically or by user interaction.

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

Example:

@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun MyUI() {
    var openBottomSheet by remember { mutableStateOf(false) }
    val coroutineScope = rememberCoroutineScope()
    val bottomSheetState = rememberModalBottomSheetState()
    val contextForToast = LocalContext.current.applicationContext

    // app content
    Column(
        modifier = Modifier.fillMaxSize(),
        horizontalAlignment = Alignment.CenterHorizontally,
        verticalArrangement = Arrangement.Center
    ) {
        Button(
            onClick = { openBottomSheet = true }
        ) {
            Text(text = "Show Bottom Sheet")
        }
    }

    // sheet content
    if (openBottomSheet) {
        ModalBottomSheet(
            onDismissRequest = { openBottomSheet = false },
            sheetState = bottomSheetState
        ) {
            LazyColumn(
                contentPadding = PaddingValues(bottom = 48.dp) // to display the last item above the navigation (system) bar
            ) {
                items(50) { count ->
                    ListItem(
                        modifier = Modifier.clickable {
                            Toast.makeText(contextForToast, "Click $count", Toast.LENGTH_SHORT)
                                .show()

                            // hide the sheet
                            coroutineScope.launch { bottomSheetState.hide() }.invokeOnCompletion {
                                if (!bottomSheetState.isVisible) {
                                    openBottomSheet = false
                                }
                            }
                        },
                        headlineContent = { Text("Item $count") },
                        leadingContent = {
                            Icon(
                                Icons.Default.Favorite,
                                contentDescription = null
                            )
                        }
                    )
                }
            }
        }
    }
}

Output:

ModalBottomSheet

Similar to the BottomSheetScaffold, the ModalBottomSheet provides the following methods:

partialExpand() – It partially expands (collapses) the sheet.

expand() – It expands the sheet.

hide() – It completely hides the sheet.

This is all about the Bottom Sheet APIs in Material 3 Jetpack Compose. I hope you have learned something new. If you have any doubts, leave a comment below.

Related Articles:


References:

Leave a Comment