返回

Compose多重回退栈与底部导航:类型安全解决方案

Android

多重回退栈与底部导航的类型安全解决方案

在Jetpack Compose中构建具有底部导航的多重回退栈时,经常会遇到一些棘手的问题。 比如,在不同的选项卡之间切换后,返回操作不会导航到上一个页面,而是回到了选项卡图的初始目标。此外,如何维护选项卡选择状态,也是一个常见的困扰。本文分析此类问题的原因,并提供对应的解决方案。

问题分析

使用 popUpTo(findStartDestination(nestedNavController.graph).id) 方法重置回退栈是造成上述问题的根源。该方法会把当前选项卡的栈弹出到起始页面,而不是用户期望的上一个页面。另外,使用 currentDestination?.hierarchy?.any { it.hasRoute(item.route::class) } 来判断选项卡是否选中是不准确的,因为处于更深层次页面的当前目标通常不在层级结构的最顶端,它不一定包含对应选项卡第一层页面的路由信息。

解决方案

针对上述问题,我们主要围绕两个目标进行优化:

  1. 正确的导航回退: 返回操作时,回到正确的上一个页面而不是起始页面。
  2. 精确的选项卡选择: 根据当前所处页面,准确更新选中状态。

导航回退优化

为了实现正确的返回导航,应该使用launchSingleTop = truesaveState = true, 同时避免 popUpTo 方法。这会保留选项卡的栈状态,并避免在切换选项卡时,将其他选项卡栈弹出到根路由。代码修改如下:

NavigationBarItem(
   selected = selectedIndex == index,
   icon = {
        Icon(
              imageVector = item.icon,
              contentDescription = null
        )
    },
    onClick = {
        selectedIndex = index
        nestedNavController.navigate(route = item.route) {
            launchSingleTop = true
            restoreState = true
         }
    }
 )

这里只保留了launchSingleToprestoreState ,移除了popUpTo, 此时在每个tab内部,navigate 可以正确更新stack并导航。返回操作时可以返回到每个tab的之前所在的页面,符合用户预期。

选项卡选中状态管理优化

要准确判断选项卡是否被选中,可以使用更精确的路由匹配逻辑,即比较当前目的地的路由与选项卡路由的父图路由:

 val selected = nestedNavController.currentDestination?.route?.startsWith(item.route.toString().substringBeforeLast("Route"))  == true

使用 startsWith() 来检查当前路由是否以对应父图路由为前缀。对于每个 NavigationBarItem, 当它的路由是当前嵌套路由目的地所在图的一个时,状态会被标记为已选择,这将准确显示当前选中的选项卡,且不会被深层路由影响。完整代码示例:

NavigationBarItem(
    selected =  nestedNavController.currentDestination?.route?.startsWith(item.route.toString().substringBeforeLast("Route"))  == true,
    icon = {
        Icon(
             imageVector = item.icon,
            contentDescription = null
        )
    },
    onClick = {
       selectedIndex = index
        nestedNavController.navigate(route = item.route) {
           launchSingleTop = true
            restoreState = true
        }
     }
)

完整代码

将上述优化合并到示例代码中:

@SuppressLint("RestrictedApi")
@Composable
private fun MainContainer(
    onGoToProfileScreen: (
        route: Any,
        navBackStackEntry: NavBackStackEntry,
    ) -> Unit,
) {
    val items = remember {
        bottomRouteDataList()
    }

    val nestedNavController = rememberNavController()
    var selectedIndex by remember {
        mutableIntStateOf(0)
    }

    Scaffold(
        modifier = Modifier.fillMaxSize(),
        topBar = {
            TopAppBar(
                title = {
                    Text("TopAppbar")
                },
                colors = TopAppBarDefaults.topAppBarColors(
                    containerColor = Color.White
                )
            )
        },
        bottomBar = {
            NavigationBar(
                modifier = Modifier.height(56.dp),
                tonalElevation = 4.dp
            ) {
                items.forEachIndexed { index, item: BottomRouteData ->
                    NavigationBarItem(
                       selected =  nestedNavController.currentDestination?.route?.startsWith(item.route.toString().substringBeforeLast("Route"))  == true,
                        icon = {
                            Icon(
                                imageVector = item.icon,
                                contentDescription = null
                            )
                        },
                        onClick = {
                            selectedIndex = index
                            nestedNavController.navigate(route = item.route) {
                                launchSingleTop = true
                                restoreState = true

                            }
                        }
                    )
                }
            }
        }
    ) { paddingValues: PaddingValues ->
        NavHost(
            modifier = Modifier.padding(paddingValues),
            navController = nestedNavController,
            startDestination = BottomNavigationRoute.HomeGraph
        ) {
            addBottomNavigationGraph(
                nestedNavController = nestedNavController,
                onGoToProfileScreen = { route, navBackStackEntry ->
                    onGoToProfileScreen(route, navBackStackEntry)
                },
                onBottomScreenClick = { route, navBackStackEntry ->
                    nestedNavController.navigate(route)
                }
            )
        }
    }
}

在上述代码中,对 NavigationBarItem 进行了修改。

结论

通过优化 launchSingleToprestoreState 和选项卡选择的逻辑,我们可以有效地管理多重回退栈和底部导航的复杂交互。 避免使用 popUpTo 可保证正确的返回行为;精确匹配父图路由,则能够实现准确的选项卡选中状态。 这些技巧有助于开发者构建出流畅且行为可预测的导航体验,尤其是在复杂的应用场景下。