英文:
Pressing back button on a ModalNavigationDrawer navigates back instead of closing the drawer
问题
我正在使用Jetpack Compose和Compose Destinations来开发Android应用。我的应用有一个ModalNavigationDrawer
。模态框通过手势打开和关闭,但当模态框打开时,我希望能够使用返回按钮来关闭它。这似乎是一种自然的操作,在我使用的几乎所有其他应用中都有效。
然而,返回按钮只是关闭(最小化)应用程序或导航到上一个屏幕。我认为这是因为Compose Destinations正在捕获返回操作并从返回堆栈中弹出。然而,在文档中一开始并没有提到如何解决这个问题。唯一提到抽屉的地方是这里,但这与目前的问题无关。
我在SO上找到了一些关于如何拦截返回按键的帖子,但对我来说这些方法似乎有点不正规。这似乎是一个基本的用例,应该有一个简单而合理的解决方案。因此,我来问一下是否有推荐的解决方法。
主要应用程序看起来像这样:
@Composable
fun App() {
val navEngine = rememberNavHostEngine()
val navController = navEngine.rememberNavController()
val startRoute = HomeRouteDestination
Surface(
modifier = Modifier.fillMaxSize(),
color = MaterialTheme.colorScheme.background
) {
DestinationsNavHost(
engine = navEngine,
navController = navController,
navGraph = NavGraphs.root,
startRoute = startRoute
)
}
}
我的抽屉组件:
private data class DrawerItem(
val icon: ImageVector,
val name: String,
val destination: DirectionDestination
)
@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun NavDrawerWrapper(
navigator: DestinationsNavigator = EmptyDestinationsNavigator,
content: @Composable (() -> Unit, () -> Unit) -> Unit
) {
val drawerState = rememberDrawerState(initialValue = DrawerValue.Closed)
val scope = rememberCoroutineScope()
val openDrawer: () -> Unit = { scope.launch { drawerState.open() } }
val closeDrawer: () -> Unit = { scope.launch { drawerState.close() } }
val items = listOf(
DrawerItem(Icons.Default.List, "Menu", HomeRouteDestination),
DrawerItem(Icons.Default.Settings, "Settings", SettingsRouteDestination)
)
var selectedItem by remember { mutableStateOf(items[0]) }
ModalNavigationDrawer(
drawerState = drawerState,
drawerContent = {
DrawerSheet(
items, selectedItem,
onSelectItem = { item ->
closeDrawer()
selectedItem = item
navigator.navigate(item.destination)
}
)
},
content = { content(openDrawer, closeDrawer) }
)
}
@OptIn(ExperimentalMaterial3Api::class)
@Composable
private fun DrawerSheet(
items: List<DrawerItem> = listOf(),
selectedItem: DrawerItem? = null,
onSelectItem: (DrawerItem) -> Unit = {}
) {
ModalDrawerSheet {
LazyColumn(modifier = Modifier.padding(horizontal = 15.dp, vertical = 20.dp)) {
items(items.size) { index ->
val item = items[index]
NavigationDrawerItem(
label = { Text(text = item.name) },
icon = { Icon(imageVector = item.icon, contentDescription = item.name) },
selected = item == selectedItem,
onClick = { onSelectItem(item) }
)
}
}
}
}
希望这可以帮助你解决问题。如果你需要更多的帮助,请随时告诉我。
英文:
I'm working on an Android app using Jetpack Compose, and Compose Destinations for navigation. My app has a ModalNavigationDrawer
. The modal opens and closes fine using gestures, but when the modal is open I'd like it to be able to close using the back button. This feels like a natural action, and works in just about any other app I've used.
Instead, the back button just closes (minimises) the app, or navigates to the previous screen. I assume this is because Compose Destinations is catching the back action and popping off the back stack. However, the documentation doesn't mention anything about solving this issue at first glance. The only time drawers are mentioned are here, but this isn't relevant to the issue at hand.
I've found a few posts on SO about how to intercept the back press, but these seem a bit hacky to me. This seems like a basic use case that should have an easy and decent solution. Hence, I came to ask if there is a recommended way of solving this.
The main app looks like this:
@Composable
fun App() {
val navEngine = rememberNavHostEngine()
val navController = navEngine.rememberNavController()
val startRoute = HomeRouteDestination
Surface(
modifier = Modifier.fillMaxSize(),
color = MaterialTheme.colorScheme.background
) {
DestinationsNavHost(
engine = navEngine,
navController = navController,
navGraph = NavGraphs.root,
startRoute = startRoute
)
}
}
My drawer component:
private data class DrawerItem(
val icon: ImageVector,
val name: String,
val destination: DirectionDestination
)
@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun NavDrawerWrapper(
navigator: DestinationsNavigator = EmptyDestinationsNavigator,
content: @Composable (() -> Unit, () -> Unit) -> Unit
) {
val drawerState = rememberDrawerState(initialValue = DrawerValue.Closed)
val scope = rememberCoroutineScope()
val openDrawer: () -> Unit = { scope.launch { drawerState.open() } }
val closeDrawer: () -> Unit = { scope.launch { drawerState.close() } }
val items = listOf(
DrawerItem(Icons.Default.List, "Menu", HomeRouteDestination),
DrawerItem(Icons.Default.Settings, "Settings", SettingsRouteDestination)
)
var selectedItem by remember { mutableStateOf(items[0]) }
ModalNavigationDrawer(
drawerState = drawerState,
drawerContent = {
DrawerSheet(
items, selectedItem,
onSelectItem = { item ->
closeDrawer()
selectedItem = item
navigator.navigate(item.destination)
}
)
},
content = { content(openDrawer, closeDrawer) }
)
}
@OptIn(ExperimentalMaterial3Api::class)
@Composable
private fun DrawerSheet(
items: List<DrawerItem> = listOf(),
selectedItem: DrawerItem? = null,
onSelectItem: (DrawerItem) -> Unit = {}
) {
ModalDrawerSheet {
LazyColumn(modifier = Modifier.padding(horizontal = 15.dp, vertical = 20.dp)) {
items(items.size) { index ->
val item = items[index]
NavigationDrawerItem(
label = { Text(text = item.name) },
icon = { Icon(imageVector = item.icon, contentDescription = item.name) },
selected = item == selectedItem,
onClick = { onSelectItem(item) }
)
}
}
}
}
答案1
得分: 1
以下是翻译的代码部分:
更短的解决方案是在每个屏幕级别需要更多自定义时使用 `BackHandler`。
BackHandler(
enabled = drawerState.isOpen,
) {
closeDrawer()
}
我有一些实用方法和可重用的 Composable 用来处理这个问题,我在我的项目中使用它。我在这里分享它供参考。根据您的需求进行更改。
由于我在整个应用程序中都使用它,所以它相当大。我有一个单一的 Scaffold,这个 Composable 对我有效。
[GitHub 链接以供探索](https://github.com/Abhimanyu14/finance-manager/blob/main/codebase/android/core/ui/src/main/java/com/makeappssimple/abhimanyu/financemanager/android/core/ui/common/BottomSheetUtil.kt)
**代码**
@Composable
fun BottomSheetHandler(
showModalBottomSheet: Boolean,
bottomSheetType: BottomSheetType,
coroutineScope: CoroutineScope,
modalBottomSheetState: ModalBottomSheetState,
keyboardController: SoftwareKeyboardController?,
resetBottomSheetType: () -> Unit,
) {
BottomSheetDisposeHandler(
modalBottomSheetState = modalBottomSheetState,
resetBottomSheetType = resetBottomSheetType,
)
BottomSheetTypeChangeHandler(
showModalBottomSheet = showModalBottomSheet,
bottomSheetType = bottomSheetType,
coroutineScope = coroutineScope,
modalBottomSheetState = modalBottomSheetState,
keyboardController = keyboardController,
)
}
@Composable
private fun BottomSheetDisposeHandler(
modalBottomSheetState: ModalBottomSheetState,
resetBottomSheetType: () -> Unit,
) {
if (modalBottomSheetState.currentValue != ModalBottomSheetValue.Hidden) {
DisposableEffect(
key1 = Unit,
) {
onDispose {
resetBottomSheetType()
}
}
}
}
@Composable
private fun BottomSheetTypeChangeHandler(
showModalBottomSheet: Boolean,
bottomSheetType: BottomSheetType,
coroutineScope: CoroutineScope,
modalBottomSheetState: ModalBottomSheetState,
keyboardController: SoftwareKeyboardController?,
) {
LaunchedEffect(
key1 = bottomSheetType,
) {
if (showModalBottomSheet) {
keyboardController?.hide()
showModalBottomSheet(
coroutineScope = coroutineScope,
modalBottomSheetState = modalBottomSheetState,
)
} else {
hideModalBottomSheet(
coroutineScope = coroutineScope,
modalBottomSheetState = modalBottomSheetState,
)
}
}
}
@Composable
internal fun BottomSheetBackHandler(
enabled: Boolean,
coroutineScope: CoroutineScope,
modalBottomSheetState: ModalBottomSheetState,
resetBottomSheetType: () -> Unit,
) {
BackHandler(
enabled = enabled,
) {
hideModalBottomSheet(
coroutineScope = coroutineScope,
modalBottomSheetState = modalBottomSheetState,
) {
resetBottomSheetType()
}
}
}
private fun showModalBottomSheet(
coroutineScope: CoroutineScope,
modalBottomSheetState: ModalBottomSheetState,
action: (() -> Unit)? = null,
) {
coroutineScope.launch {
if (modalBottomSheetState.currentValue == modalBottomSheetState.targetValue) {
modalBottomSheetState.show()
action?.invoke()
}
}
}
private fun hideModalBottomSheet(
coroutineScope: CoroutineScope,
modalBottomSheetState: ModalBottomSheetState,
action: (() -> Unit)? = null,
) {
coroutineScope.launch {
if (modalBottomSheetState.currentValue == modalBottomSheetState.targetValue) {
modalBottomSheetState.hide()
action?.invoke()
}
}
}
**用法**
BottomSheetBackHandler(
enabled = bottomSheetType != HomeBottomSheetType.NONE,
coroutineScope = coroutineScope,
modalBottomSheetState = sheetState,
resetBottomSheetType = onBackPress,
)
我使用一个枚举来存储当前的底部抽屉类型。
英文:
The shorter version of the solution is to use BackHandler
if you need more customization per screen level.
BackHandler(
enabled = drawerState.isOpen,
) {
closeDrawer()
}
I have some util methods and reusable Composable to handle this which I use in my project. I am sharing the same here for reference. Change it according to your requirement.
It is quite large as I use it for my whole app. I have a single Scaffold and this composable works for me.
Code
@Composable
fun BottomSheetHandler(
showModalBottomSheet: Boolean,
bottomSheetType: BottomSheetType,
coroutineScope: CoroutineScope,
modalBottomSheetState: ModalBottomSheetState,
keyboardController: SoftwareKeyboardController?,
resetBottomSheetType: () -> Unit,
) {
BottomSheetDisposeHandler(
modalBottomSheetState = modalBottomSheetState,
resetBottomSheetType = resetBottomSheetType,
)
BottomSheetTypeChangeHandler(
showModalBottomSheet = showModalBottomSheet,
bottomSheetType = bottomSheetType,
coroutineScope = coroutineScope,
modalBottomSheetState = modalBottomSheetState,
keyboardController = keyboardController,
)
}
@Composable
private fun BottomSheetDisposeHandler(
modalBottomSheetState: ModalBottomSheetState,
resetBottomSheetType: () -> Unit,
) {
if (modalBottomSheetState.currentValue != ModalBottomSheetValue.Hidden) {
DisposableEffect(
key1 = Unit,
) {
onDispose {
resetBottomSheetType()
}
}
}
}
@Composable
private fun BottomSheetTypeChangeHandler(
showModalBottomSheet: Boolean,
bottomSheetType: BottomSheetType,
coroutineScope: CoroutineScope,
modalBottomSheetState: ModalBottomSheetState,
keyboardController: SoftwareKeyboardController?,
) {
LaunchedEffect(
key1 = bottomSheetType,
) {
if (showModalBottomSheet) {
keyboardController?.hide()
showModalBottomSheet(
coroutineScope = coroutineScope,
modalBottomSheetState = modalBottomSheetState,
)
} else {
hideModalBottomSheet(
coroutineScope = coroutineScope,
modalBottomSheetState = modalBottomSheetState,
)
}
}
}
@Composable
internal fun BottomSheetBackHandler(
enabled: Boolean,
coroutineScope: CoroutineScope,
modalBottomSheetState: ModalBottomSheetState,
resetBottomSheetType: () -> Unit,
) {
BackHandler(
enabled = enabled,
) {
hideModalBottomSheet(
coroutineScope = coroutineScope,
modalBottomSheetState = modalBottomSheetState,
) {
resetBottomSheetType()
}
}
}
private fun showModalBottomSheet(
coroutineScope: CoroutineScope,
modalBottomSheetState: ModalBottomSheetState,
action: (() -> Unit)? = null,
) {
coroutineScope.launch {
if (modalBottomSheetState.currentValue == modalBottomSheetState.targetValue) {
modalBottomSheetState.show()
action?.invoke()
}
}
}
private fun hideModalBottomSheet(
coroutineScope: CoroutineScope,
modalBottomSheetState: ModalBottomSheetState,
action: (() -> Unit)? = null,
) {
coroutineScope.launch {
if (modalBottomSheetState.currentValue == modalBottomSheetState.targetValue) {
modalBottomSheetState.hide()
action?.invoke()
}
}
}
Usage
BottomSheetBackHandler(
enabled = = bottomSheetType != HomeBottomSheetType.NONE,
coroutineScope = coroutineScope,
modalBottomSheetState = sheetState,
resetBottomSheetType = onBackPress,
)
I use an enum to store the current bottom sheet type.
通过集体智慧和协作来改善编程学习和解决问题的方式。致力于成为全球开发者共同参与的知识库,让每个人都能够通过互相帮助和分享经验来进步。
评论