Blog Infos
Author
Published
Topics
, , , ,
Published
Best to check before closing the bottom sheet!

The ModalBottomSheet in Jetpack Compose is easy to use, it just pops up at the bottom of the screen and allows the user easy interaction without covering the whole screen and blocking content.

Fruit photo by Alexander Schimmeck on Unsplash

The problem is, when the user accidentally taps out of the sheet or closes it they may lose any input they have added or changes they have made.

If we were using a custom close button then it would be easy, simply show a dialog when the user presses the button and only perform the sheet hide when the user has confirmed. But what about the bottom sheet handle, swiping down the bottom sheet or tapping outside on the scrim?

Fear not! It is pretty straight forward.

Adding a confirmation dialog on ModalBottomSheet close

First, we have the basic building blocks of our ModalBottomSheet:

val coroutineScope = rememberCoroutineScope()
var items by rememberSaveable { mutableStateOf(listOf<String>()) }
var showAddSheet by rememberSaveable { mutableStateOf(false) }
val addSheetState = rememberModalBottomSheetState()
Scaffold(...) { innerPadding ->
// Screen content
ShoppingList(
items = items,
onAddItemClick = { showAddSheet = true },
modifier = Modifier.fillMaxSize().padding(innerPadding)
)
// Bottom sheet
if (showAddSheet) {
ModalBottomSheet(
onDismissRequest = { showAddSheet = false },
sheetState = addSheetState,
modifier = Modifier
) {
BottomSheetContent(
onSaveClick = {
// Save the value
items = items.plus(it)
// Close the sheet, allowing the animation to run
coroutineScope.launch {
addSheetState.hide()
}.invokeOnCompletion {
if (!addSheetState.isVisible) {
showAddSheet = false
}
}
}
)
}
}
}
view raw MainActivity.kt hosted with ❤ by GitHub

Here we have a simple Boolean state to hold whether the ModalBottomSheet should be displayed or not, showAddSheet, the ModalBottomSheet also takes in addSheetState which is initialised & tracked with rememberModalBottomSheetState.

We also need to create a AlertDialog to be the confirmation dialog to show when the user tries to close the ModalBottomSheet:

val coroutineScope = rememberCoroutineScope()
var showConfirmChanges by rememberSaveable { mutableStateOf(false) }
...
if (showConfirmChanges) {
AlertDialog(
onDismissRequest = { showConfirmChanges = false },
title = { Text(stringResource(R.string.confirm_changes_title)) },
text = { Text(stringResource(R.string.confirm_changes_text)) },
confirmButton = {
TextButton(onClick = {
// Close the ModalBottomSheet
coroutineScope.launch {
addSheetState.hide()
}.invokeOnCompletion {
if (!addSheetState.isVisible) {
showAddSheet = false
}
}
// Close the confirm changes AlertDialog
showConfirmChanges = false
}) {
Text(stringResource(R.string.confirm_changes_confirm))
}
},
dismissButton = {
TextButton(onClick = {
showConfirmChanges = false
}) {
Text(stringResource(R.string.confirm_changes_dismiss))
}
},
properties = DialogProperties(
dismissOnBackPress = false,
dismissOnClickOutside = false
)
)
}
view raw MainActivity.kt hosted with ❤ by GitHub

Both the dialog and the save button in the ModalBottomSheet includes the closing of the ModalBottomSheet using the addSheetState.hide() method to allow the hide animation to run before fully removing the sheet from the composition via the showAddSheet state.

Observe the value change

Now we have the basic components the next thing to do is look at the rememberModalBottomSheetState. This takes in a callback confirmValueChange which we can confirm or veto a pending sheet state change. The change here is simple, we need to check if the requested sheet value is Hidden (the other values are Expanded and PartiallyExpanded — we don’t care about these when showing the confirmation dialog) and if so, show the dialog. Then we need to return a Boolean to indicate whether the sheet should take the requested action or not.

val addSheetState = rememberModalBottomSheetState(
confirmValueChange = {
if (it == SheetValue.Hidden) {
showConfirmChanges = true
false
} else {
// We're expanding the sheet so we always return true
true
}
}
)
view raw MainActivity.kt hosted with ❤ by GitHub

OUR VIDEO RECOMMENDATION

, ,

Migrating to Jetpack Compose – an interop love story

Most of you are familiar with Jetpack Compose and its benefits. If you’re able to start anew and create a Compose-only app, you’re on the right track. But this talk might not be for you…
Watch Video

Migrating to Jetpack Compose - an interop love story

Simona Milanovic
Android DevRel Engineer for Jetpack Compose
Google

Migrating to Jetpack Compose - an interop love story

Simona Milanovic
Android DevRel Engin ...
Google

Migrating to Jetpack Compose - an interop love story

Simona Milanovic
Android DevRel Engineer f ...
Google

Jobs

No results found.

So, since we want to show the AlertDialog we just set the showConfirmChanges state to true and return false from the confirmValueChange callback to prevent the action being carried out. If the requested sheet state value is not Hidden then just return true so it is carried out unaffected.

This will be run whenever the user touches outside of the ModalBottomSheet or tries to swipe down on it using the handle. It won’t get called if a standard back press is used (as that is handled by the navigation architecture) but that can be overridden in a similar manner using OnBackPressedDispatcher.

And that’s it! Simple!

Finally, we have the full solution:

val coroutineScope = rememberCoroutineScope()
var items by rememberSaveable { mutableStateOf(listOf<String>()) }
var showAddSheet by rememberSaveable { mutableStateOf(false) }
var showConfirmChanges by rememberSaveable { mutableStateOf(false) }
val addSheetState = rememberModalBottomSheetState(
confirmValueChange = {
if (it == SheetValue.Hidden) {
showConfirmChanges = true
false
} else {
// We're expanding the sheet so we always return true
true
}
}
)
Scaffold(...) { innerPadding ->
ShoppingList(
items = items,
onAddItemClick = { showAddSheet = true },
modifier = Modifier.fillMaxSize().padding(innerPadding)
)
if (showAddSheet) {
ModalBottomSheet(
onDismissRequest = { showConfirmChanges = true },
sheetState = addSheetState,
modifier = Modifier
) {
BottomSheetContent(
onSaveClick = {
items = items.plus(it)
coroutineScope.launch {
addSheetState.hide()
}.invokeOnCompletion {
if (!addSheetState.isVisible) {
showAddSheet = false
}
}
}
)
}
}
if (showConfirmChanges) {
AlertDialog(
onDismissRequest = { showConfirmChanges = false },
title = { Text(stringResource(R.string.confirm_changes_title)) },
text = { Text(stringResource(R.string.confirm_changes_text)) },
confirmButton = {
TextButton(onClick = {
coroutineScope.launch {
addSheetState.hide()
}.invokeOnCompletion {
if (!addSheetState.isVisible) {
showAddSheet = false
}
}
showConfirmChanges = false
}) {
Text(stringResource(R.string.confirm_changes_confirm))
}
},
dismissButton = {
TextButton(onClick = {
showConfirmChanges = false
}) {
Text(stringResource(R.string.confirm_changes_dismiss))
}
},
properties = DialogProperties(
dismissOnBackPress = false,
dismissOnClickOutside = false
)
)
}
}
view raw MainActivity.kt hosted with ❤ by GitHub

All the code from this post can be found on my Github:

https://github.com/KatieBarnett/Experiments/tree/main/jc-modalbottomsheet?source=post_page—–639049decbbf——————————–

This is previously published on proandroiddev.com.

YOU MAY BE INTERESTED IN

YOU MAY BE INTERESTED IN

blog
It’s one of the common UX across apps to provide swipe to dismiss so…
READ MORE
blog
In this part of our series on introducing Jetpack Compose into an existing project,…
READ MORE
blog
In the world of Jetpack Compose, where designing reusable and customizable UI components is…
READ MORE
blog

How to animate BottomSheet content using Jetpack Compose

Early this year I started a new pet project for listening to random radio…
READ MORE

Leave a Reply

Your email address will not be published. Required fields are marked *

Fill out this field
Fill out this field
Please enter a valid email address.

Menu