androidandroid-jetpack-composejetpack-compose-modalbottomsheet

Back press handler in ModalBottomSheet in Compose M3 1.1


The new M3 1.1 compose brings the ModalBottomSheet, however a BackHandler doesn't work when it is visible. How can this be implemented? I want a backpress to close the app, and not the modalsheet.


Solution

  • This is a bug in ModalBottomSheet, and even though Google marked it as fixed on Oct 31, 2023 and released it, the issue still occurs on Material3 1.2.0.

    I have found a workaround though. It consists in intercepting the key event, doing the same checks that ModalBottomSheetWindow.dispatchKeyEvent would do, and then delegating that to the OnBackPressedDispatcherOwner manually.

    @Composable
    fun ModalBottomSheetWithBackHandling(
        onDismissRequest: () -> Unit,
        modifier: Modifier = Modifier,
        sheetState: SheetState = rememberModalBottomSheetState(),
        properties: ModalBottomSheetProperties = ModalBottomSheetDefaults.properties(),
        content: @Composable ColumnScope.() -> Unit,
    ) {
        val scope = rememberCoroutineScope()
    
        BackHandler(enabled = sheetState.targetValue != SheetValue.Hidden) {
            // Always catch back here, but only let it dismiss if shouldDismissOnBackPress.
            // If not, it will have no effect.
            if (properties.shouldDismissOnBackPress) {
                scope.launch { sheetState.hide() }.invokeOnCompletion {
                    if (!sheetState.isVisible) {
                        onDismissRequest()
                    }
                }
            }
        }
    
        val requester = remember { FocusRequester() }
        val backPressedDispatcherOwner = LocalOnBackPressedDispatcherOwner.current
    
        ModalBottomSheet(
            onDismissRequest = onDismissRequest,
            modifier = modifier
                .focusRequester(requester)
                .focusable()
                .onPreviewKeyEvent {
                    if (it.key == Key.Back && it.type == KeyEventType.KeyUp && !it.nativeKeyEvent.isCanceled) {
                        backPressedDispatcherOwner?.onBackPressedDispatcher?.onBackPressed()
                        return@onPreviewKeyEvent true
                    }
                    return@onPreviewKeyEvent false
                },
            properties = ModalBottomSheetDefaults.properties(
                securePolicy = properties.securePolicy,
                isFocusable = properties.isFocusable,
                // Set false otherwise the onPreviewKeyEvent doesn't work at all.
                // The functionality of shouldDismissOnBackPress is achieved by the BackHandler.
                shouldDismissOnBackPress = false,
            ),
            content = content,
        )
    
        LaunchedEffect(Unit) {
            requester.requestFocus()
        }
    }