This article answers:
-
How to handle back press for:
ModalBottomSheet, AlertDialogor any other modal window. - What to do if there are several modals on the screen.
The proposed approach is universal and can be applied to other similar cases.
Let’s dive into it. Here is the main utility class:
/**
* This class helps to unitize back navigation. There can be several folded BackNavElements.
*
* Each time user presses the Back button, the whole chain is asked one-by-one starting from the most child node
* whether any handler needs to process itself. In case it needs - it processes and returns the Result.CANNOT_GO_BACK.
* Whole chain do not proceed if ANY of the elements returns the Result.CANNOT_GO_BACK.
*
* So, the chain is processed only if ALL of its sub handles returned the Result.CAN_GO_BACK.
*
* This utilize the cases when user has several dialogs on the screen that needs to be closed
* one-by-one.
*/
class BackNavElement private constructor(
private var child: BackNavElement? = null,
private val handler: () -> Result
) {
enum class Result {
CANNOT_GO_BACK,
CAN_GO_BACK
}
/**
* Adds element to the END of the chain.
*/
fun add(element: BackNavElement?) {
this.child?.let {
it.add(element)
} ?: run {
this.child = element
}
}
fun tryGoBack(): Result {
if (child?.tryGoBack() == Result.CANNOT_GO_BACK) {
return Result.CANNOT_GO_BACK
}
return handler()
}
companion object {
fun default(child: BackNavElement? = null, handler: () -> Unit) =
BackNavElement(
child = child,
handler = {
handler()
BackNavElement.Result.CAN_GO_BACK
})
fun needsProcessing(child: BackNavElement? = null, handler: () -> Result) =
BackNavElement(child = child, handler = handler)
}
}
Main takeaway points:
BackNavElementclass allows organizing a linked list of handlers.- Handler — is a callback that must return Result.
Resultcan beCANNOT_GO_BACK orCAN_GO_BACK.addfunction adds a new element to the end of the chain.companion objecthas convenient fabric methods forBackNavElementcreation.
Now let’s look at the example to understand how we can take advantage of it.
Confirmation dialog
Use case:
- The user is on the screen doing some editing/other important operation.
- The user presses the Back button.
- The app shows a confirmation dialog.
- If the user confirms the exit — the app navigates back.
- If the user dismisses the dialog or cancels the exit — the app hides the dialog and stays on the screen.
ExitConfirmDialog.kt
@Composable
fun ExitDialog(@StringRes bodyResId: Int, onNavIconClicked: () -> Unit): BackNavElement {
val showDialog = rememberMutableStateOf(value = false)
val defaultBackHandler = BackNavElement.needsProcessing {
if (!showDialog.value) {
showDialog.value = true
BackNavElement.Result.CANNOT_GO_BACK
} else {
BackNavElement.Result.CAN_GO_BACK
}
}
if (showDialog.value) {
DefaultAlertDialog(
onDismiss = { showDialog.value = false },
titleResId = R.string.exit_confirmation_title,
bodyResId = bodyResId,
positiveButtonResId = R.string.confirm,
onPositiveClick = {
showDialog.value = false
onNavIconClicked()
},
negativeButtonResId = R.string.cancel
)
}
return defaultBackHandler
}
Client Composablecontent:
val rootBackHandler = ExitDialog(
bodyResId = R.string.exit_confirmation_body,
onNavIconClicked = onNavIconClicked
)
DefaultBackHandler(rootBackHandler)
// other views such as Text, Icon, etc.
Here onNavIconClicked is a callback that does navController.popBackStack() on a higher level.
DefaultBackHandler:
@Composable
fun DefaultBackHandler(backNavElement: BackNavElement) = BackHandler { backNavElement.tryGoBack() }
Where BackHandler is the default Composable provided by the framework.
What happens here?
- The user presses the Back button.
- The app checks if the dialog is not shown — it shows it.
- If the dialog is already shown — it will be dismissed.
So the only way to go back is to press theConfirm button on AlertDialog.
Let’s look at another example.
ModalBottomSheet dialog
Use case:
There is a ModalBottomSheet dialog on the screen. It may be hidden or shown.
- The user presses the Back button while the dialog is shown — dialog changes its state to hidden.
- The user presses the Back button while the dialog is hidden —
onNavIconClickedis invoked.
To archive it the only thing that needs to be added to the client code is:
DefaultBackHandler(
BackNavElement.default(
child = modalBackNavElement(modalState, coroutineScope),
handler = onNavIconClicked
)
)
Where modalBackNavElement() is a helper method:
@OptIn(ExperimentalMaterialApi::class)
fun modalBackNavElement(
state: ModalBottomSheetState,
coroutineScope: CoroutineScope,
callback: (() -> Unit)? = null
) = BackNavElement.needsProcessing {
if (state.isVisible) {
state.hideAnd(coroutineScope = coroutineScope, thenCallback = { callback?.invoke() })
BackNavElement.Result.CANNOT_GO_BACK
} else {
BackNavElement.Result.CAN_GO_BACK
}
}
hideAnd():
@OptIn(ExperimentalMaterialApi::class)
fun ModalBottomSheetState.hideAnd(
coroutineScope: CoroutineScope,
thenCallback: () -> Unit
) {
coroutineScope.launch {
if (isVisible) {
hide()
thenCallback()
}
}
}
That’s it! The next example shows how to build chains.
Chains
Imagine that the screen has several states. They are represented as separate Composable functions. The appropriate Composable function is shown depending on the view state.
The Composable ChildComposable2 has 2 modals. They need to be correctly handled when the user presses the Back button.
So the screen root Composable contains:
val rootBackHandler = ExitDialog(
bodyResId = R.string.exit_confirmation_body,
onNavIconClicked = onNavIconClicked
)
DefaultBackHandler(rootBackHandler)
Job Offers
Then depending on the state ChildComposable2 may be added into the composition.
@Composable
fun ChildComposable2(
rootBackHandler: BackNavElement,
// other parameters
) {
rootBackHandler.apply {
add(modalBackNavElement(addWalletModalState, coroutineScope))
add(modalBackNavElement(walletsListModalState, coroutineScope))
// other views such as Text, Icon, etc.
}
}
As you see, the only thing that is needed is to pass rootBackHandler to the child Composable. This Composable adds its handlers via add() method.
The whole chain will be processed in the next priority — walletsListModalState, addWalletModalState, rootBackHandler .
This can be scaled to any number of child Composables or modal windows.
This is an example how several modals on the same screen can be properly handled
Conclusion
Although you may think that the solution is complex actually it’s already simplified. Previously I had children instead of child in the BackNavElementclass. However, with some time passed I realized that it was overkill for my app. But if you have heavy and complex screens — you may find it useful for your use cases.
I think that maybe some libraries exist to utilize all the work. If not — write a comment, perhaps I will do some.
If you know simpler (but still flexible) solutions — let me know.
Happy coding! Follow me on Twitter.
This article was originally published on proandroiddev.com on March 10, 2022


