使用 Jetpack Compose 中的多个返回堆栈 BottomNavigation 和类型安全更改返回堆栈

问题描述 投票:0回答:1

我构建了一个带有底部导航的多个后退堆栈导航示例,但是当从另一个选项卡/导航应用程序单击后退时,导航到图形的初始目的地而不是后退堆栈上的最后一个目的地。

如 gif 所示,主屏幕存储状态和带有保存和恢复状态的返回堆栈,但是当单击返回时,它会弹出到 HomeScreen1,而返回堆栈包含 HomeScreen3。然而,更改到另一个选项卡并返回主页会恢复状态。

enter image description here

很明显这是因为

popUpTo(findStartDestination(nestedNavController.graph).id)

弹出或存储后退堆栈的正确方法是什么,并且在后按时导航到正确的页面?

导航图

private fun NavGraphBuilder.addBottomNavigationGraph(
    nestedNavController: NavHostController,
    onScreenClick: (route: Any, navBackStackEntry: NavBackStackEntry) -> Unit,
) {
    navigation<BottomNavigationRoute.HomeRoute>(
        startDestination = BottomNavigationRoute.HomeRoute1
    ) {
        composable<BottomNavigationRoute.HomeRoute1> { from: NavBackStackEntry ->
            Screen(
                text = "Home Screen1",
                navController = nestedNavController,
                onClick = {
                    nestedNavController.navigate(BottomNavigationRoute.HomeRoute2)
                }
            )
        }

        composable<BottomNavigationRoute.HomeRoute2> { from: NavBackStackEntry ->
            Screen(
                text = "Home Screen2",
                navController = nestedNavController,
                onClick = {
                    nestedNavController.navigate(BottomNavigationRoute.HomeRoute3)
                }
            )
        }

        composable<BottomNavigationRoute.HomeRoute3> { from: NavBackStackEntry ->
            Screen(
                text = "Home Screen3",
                navController = nestedNavController
            )
        }
    }

    navigation<BottomNavigationRoute.SettingsRoute>(
        startDestination = BottomNavigationRoute.SettingsRoute1
    ) {
        composable<BottomNavigationRoute.SettingsRoute1> { from: NavBackStackEntry ->
            Screen(
                text = "Settings Screen",
                navController = nestedNavController,
                onClick = {
                    nestedNavController.navigate(BottomNavigationRoute.SettingsRoute2)
                }
            )
        }

        composable<BottomNavigationRoute.SettingsRoute2> { from: NavBackStackEntry ->
            Screen(
                text = "Settings Screen2",
                navController = nestedNavController,
                onClick = {
                    nestedNavController.navigate(BottomNavigationRoute.SettingsRoute3)
                }
            )
        }

        composable<BottomNavigationRoute.SettingsRoute3> { from: NavBackStackEntry ->
            Screen(
                text = "Settings Screen3",
                navController = nestedNavController
            )
        }
    }

    composable<BottomNavigationRoute.FavoritesRoute> { from: NavBackStackEntry ->
        Screen(
            text = "Favorites Screen",
            navController = nestedNavController,
            onClick = {
                onScreenClick(
                    Profile("Favorites"),
                    from
                )
            }
        )
    }

    composable<BottomNavigationRoute.NotificationRoute> { from: NavBackStackEntry ->
        Screen(
            text = "Notifications Screen",
            navController = nestedNavController,
            onClick = {
                onScreenClick(
                    Profile("Notifications"),
                    from
                )
            }
        )
    }
}

根和嵌套 NavHost 组合

@Preview
@Composable
fun NavigationTest() {
    val navController = rememberNavController()

    NavHost(
        modifier = Modifier.fillMaxSize(),
        navController = navController,
        startDestination = BottomNavigationRoute.DashboardRoute,
        enterTransition = {
            slideIntoContainer(
                towards = SlideDirection.Start,
                animationSpec = tween(700)
            )
        },
        exitTransition = {
            slideOutOfContainer(
                towards = SlideDirection.End,
                animationSpec = tween(700)
            )
        },
        popEnterTransition = {
            slideIntoContainer(
                towards = SlideDirection.Start,
                animationSpec = tween(700)
            )
        },
        popExitTransition = {
            slideOutOfContainer(
                towards = SlideDirection.End,
                animationSpec = tween(700)
            )
        }
    ) {

        composable<BottomNavigationRoute.DashboardRoute> {
            MainContainer { route: Any, navBackStackEntry: NavBackStackEntry ->
                // Navigate only when life cycle is resumed for current screen
                if (navBackStackEntry.lifecycleIsResumed()) {
                    navController.navigate(route = route)
                }
            }
        }

        composable<Profile> { navBackStackEntry: NavBackStackEntry ->
            val profile: Profile = navBackStackEntry.toRoute<Profile>()
            Screen(profile.toString(), navController)
        }
    }
}

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

    val nestedNavController = rememberNavController()

    val navBackStackEntry: NavBackStackEntry? by nestedNavController.currentBackStackEntryAsState()
    val currentDestination = navBackStackEntry?.destination

    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.forEach { item: BottomRouteData ->

                    // Checks destination's route with type safety
                    val selected =
                        currentDestination?.hierarchy?.any { it.hasRoute(item.route::class) } == true

                    NavigationBarItem(
                        selected = selected,
                        icon = {
                            Icon(
                                imageVector = item.icon,
                                contentDescription = null
                            )
                        },
                        onClick = {

                            // This is for not opening same screen if current destination
                            // is equal to target destination
                            if (selected.not()) {

                                nestedNavController.navigate(route = item.route) {
                                    launchSingleTop = true

                                    // 🔥 If restoreState = true and saveState = true are commented
                                    // routes other than Home1 are not saved
                                    restoreState = true

                                    // Pop up backstack to the first destination and save state.
                                    // This makes going back
                                    // to the start destination when pressing back in any other bottom tab.
                                    popUpTo(findStartDestination(nestedNavController.graph).id) {
                                        saveState = true
                                    }
                                }
                            }
                        }
                    )
                }
            }
        }
    ) { paddingValues: PaddingValues ->
        NavHost(
            modifier = Modifier.padding(paddingValues),
            navController = nestedNavController,
            startDestination = BottomNavigationRoute.HomeRoute
        ) {
            addBottomNavigationGraph(nestedNavController) { route, navBackStackEntry ->
                onScreenClick(route, navBackStackEntry)
            }
        }
    }
}

用于显示和跟踪当前返回堆栈的屏幕

@SuppressLint("RestrictedApi")
@Composable
private fun Screen(
    text: String,
    navController: NavController,
    onClick: (() -> Unit)? = null,
) {

    val packageName = LocalContext.current.packageName

    var counter by rememberSaveable {
        mutableIntStateOf(0)
    }

    Column(
        modifier = Modifier
            .background(MaterialTheme.colorScheme.surface)
            .fillMaxSize()
            .padding(16.dp),
        verticalArrangement = Arrangement.spacedBy(16.dp)
    ) {
        Text(
            text = text,
            fontSize = 26.sp,
            fontWeight = FontWeight.Bold
        )

        Button(
            modifier = Modifier.fillMaxWidth(),
            onClick = {
                counter++
            }
        ) {
            Text("Counter: $counter")
        }

        onClick?.let {
            Button(
                modifier = Modifier.fillMaxWidth(),
                onClick = {
                    onClick()
                }
            ) {
                Text("Navigate next screen")
            }
        }

        val currentBackStack: List<NavBackStackEntry> by navController.currentBackStack.collectAsState()

        LazyColumn(
            modifier = Modifier.fillMaxSize(),
            verticalArrangement = Arrangement.spacedBy(8.dp)
        ) {

            // Don't do looped operations in actual code, it's for demonstration
            items(items = currentBackStack.reversed()) {
                Text(
                    text = it.destination.route
                        ?.replace("$packageName.", "")
                        ?.replace(
                            "BottomNavigationRoute.",
                            ""
                        ) ?: it.destination.displayName,
                    modifier = Modifier
                        .shadow(4.dp, RoundedCornerShape(8.dp))
                        .background(Color.White)
                        .fillMaxWidth()
                        .padding(16.dp),
                    fontSize = 18.sp
                )
            }
        }
    }
}

路线和路线数据

@Serializable
sealed class BottomNavigationRoute {

    @Serializable
    data object DashboardRoute : BottomNavigationRoute()

    @Serializable
    data object HomeRoute : BottomNavigationRoute()

    @Serializable
    data object HomeRoute1 : BottomNavigationRoute()

    @Serializable
    data object HomeRoute2 : BottomNavigationRoute()

    @Serializable
    data object HomeRoute3 : BottomNavigationRoute()

    @Serializable
    data object SettingsRoute : BottomNavigationRoute()

    @Serializable
    data object SettingsRoute1 : BottomNavigationRoute()

    @Serializable
    data object SettingsRoute2 : BottomNavigationRoute()

    @Serializable
    data object SettingsRoute3 : BottomNavigationRoute()

    @Serializable
    data object FavoritesRoute : BottomNavigationRoute()

    @Serializable
    data object NotificationRoute : BottomNavigationRoute()
}

internal fun bottomRouteDataList() = listOf(
    BottomRouteData(
        title = "Home",
        icon = Icons.Default.Home,
        route = BottomNavigationRoute.HomeRoute
    ),
    BottomRouteData(
        title = "Settings",
        icon = Icons.Default.Settings,
        route = BottomNavigationRoute.SettingsRoute
    ),
    BottomRouteData(
        title = "Favorites",
        icon = Icons.Default.Favorite,
        route = BottomNavigationRoute.FavoritesRoute
    ),
    BottomRouteData(
        title = "Notifications",
        icon = Icons.Default.Notifications,
        route = BottomNavigationRoute.NotificationRoute
    )
)

data class BottomRouteData(
    val title: String,
    val icon: ImageVector,
    val route: BottomNavigationRoute,
)

JetSnack 的导航功能

/**
 * If the lifecycle is not resumed it means this NavBackStackEntry already processed a nav event.
 *
 * This is used to de-duplicate navigation events.
 */
internal fun NavBackStackEntry.lifecycleIsResumed() =
    this.lifecycle.currentState == Lifecycle.State.RESUMED

private val NavGraph.startDestination: NavDestination?
    get() = findNode(startDestinationId)

/**
 * Copied from similar function in NavigationUI.kt
 *
 * https://cs.android.com/androidx/platform/frameworks/support/+/androidx-main:navigation/navigation-ui/src/main/java/androidx/navigation/ui/NavigationUI.kt
 */
internal tailrec fun findStartDestination(graph: NavDestination): NavDestination {
    return if (graph is NavGraph) findStartDestination(graph.startDestination!!) else graph
}
android android-jetpack-compose jetpack-compose-navigation
1个回答
0
投票

文档在这里介绍了这个用例https://developer.android.com/develop/ui/compose/navigation#nested-nav

重要的部分是,当您弹出到图表的根部时,您需要保存状态,当您返回到另一个选项卡时,您需要恢复状态。这是通过

saveState = true
restoreState = true
完成的,如以下代码片段所示:

navController.navigate(topLevelRoute.route) {
    // Pop up to the start destination of the graph to
    // avoid building up a large stack of destinations
    // on the back stack as users select items
    popUpTo(navController.graph.findStartDestination().id) {
        saveState = true
    }
    // Avoid multiple copies of the same destination when
    // reselecting the same item
    launchSingleTop = true
    // Restore state when reselecting a previously selected item
    restoreState = true
}
© www.soinside.com 2019 - 2024. All rights reserved.