我构建了一个带有底部导航的多个后退堆栈导航示例,但是当从另一个选项卡/导航应用程序单击后退时,导航到图形的初始目的地而不是后退堆栈上的最后一个目的地。
如 gif 所示,主屏幕存储状态和带有保存和恢复状态的返回堆栈,但是当单击返回时,它会弹出到 HomeScreen1,而返回堆栈包含 HomeScreen3。然而,更改到另一个选项卡并返回主页会恢复状态。
很明显这是因为
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
}
文档在这里介绍了这个用例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
}