Как видно на gif-изображении, главный экран хранит состояния и задний стек с состояниями сохранения и восстановления, но при нажатии кнопки «Назад» он перемещается на HomeScreen1, а задний стек содержит HomeScreen3. Однако переход на другую вкладку и возврат на главную восстанавливает состояние.

Очевидно, из-за этого
popUpTo(findStartDestination(nestedNavController.graph).id)
Как правильно извлечь или сохранить обратную стопку, по которой она также перемещается на правильную страницу при обратном нажатии?
Выбрано также не работает с
val selected =
currentDestination?.hierarchy?.any { it.hasRoute(item.route::class) } == true
потому что вторая или третья страница на вкладке не содержит первого экрана при проверке иерархии. Это еще одна проблема со вложенной навигацией: использование индексов для нажатого элемента работает, но затем при нажатии назад он не перемещается в правильный индекс.
Добавлен обратный стек и иерархия, чтобы наблюдать, как они изменять. Я попробовал несколько комбинаций с ним, но так и не нашел подходящего способа правильно сохранить и восстановить состояние первых вкладок с помощью подхода сохранения/восстановления.
Навигационный график
private fun NavGraphBuilder.addBottomNavigationGraph(
nestedNavController: NavHostController,
onGoToProfileScreen: (route: Any, navBackStackEntry: NavBackStackEntry) -> Unit,
onBottomScreenClick: (route: Any, navBackStackEntry: NavBackStackEntry) -> Unit,
) {
navigation(
startDestination = BottomNavigationRoute.HomeRoute1
) {
composable { from: NavBackStackEntry ->
Screen(
text = "Home Screen1",
navController = nestedNavController,
onClick = {
onBottomScreenClick(BottomNavigationRoute.HomeRoute2, from)
}
)
}
composable { from: NavBackStackEntry ->
Screen(
text = "Home Screen2",
navController = nestedNavController,
onClick = {
onBottomScreenClick(BottomNavigationRoute.HomeRoute3, from)
}
)
}
composable { from: NavBackStackEntry ->
Screen(
text = "Home Screen3",
navController = nestedNavController
)
}
}
navigation(
startDestination = BottomNavigationRoute.SettingsRoute1
) {
composable { from: NavBackStackEntry ->
Screen(
text = "Settings Screen",
navController = nestedNavController,
onClick = {
onBottomScreenClick(BottomNavigationRoute.SettingsRoute2, from)
}
)
}
composable { from: NavBackStackEntry ->
Screen(
text = "Settings Screen2",
navController = nestedNavController,
onClick = {
onBottomScreenClick(BottomNavigationRoute.SettingsRoute3, from)
}
)
}
composable { from: NavBackStackEntry ->
Screen(
text = "Settings Screen3",
navController = nestedNavController
)
}
}
composable { from: NavBackStackEntry ->
Screen(
text = "Favorites Screen",
navController = nestedNavController,
onClick = {
onGoToProfileScreen(
Profile("Favorites"),
from
)
}
)
}
composable { from: NavBackStackEntry ->
Screen(
text = "Notifications Screen",
navController = nestedNavController,
onClick = {
onGoToProfileScreen(
Profile("Notifications"),
from
)
}
)
}
}
Корневые и вложенные составные элементы NavHost
@SuppressLint("RestrictedApi")
@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 {
MainContainer { route: Any, navBackStackEntry: NavBackStackEntry ->
// Navigate only when life cycle is resumed for current screen
if (navBackStackEntry.lifecycleIsResumed()) {
navController.navigate(route = route)
}
}
}
composable
{ navBackStackEntry: NavBackStackEntry ->
val profile: Profile = navBackStackEntry.toRoute()
Screen(profile.toString(), navController)
}
}
}
@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 = selectedIndex == index,
icon = {
Icon(
imageVector = item.icon,
contentDescription = null
)
},
onClick = {
selectedIndex = index
nestedNavController.navigate(route = item.route) {
launchSingleTop = true
//
// 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.HomeGraph
) {
addBottomNavigationGraph(
nestedNavController = nestedNavController,
onGoToProfileScreen = { route, navBackStackEntry ->
onGoToProfileScreen(route, navBackStackEntry)
},
onBottomScreenClick = { route, navBackStackEntry ->
nestedNavController.navigate(route)
}
)
}
}
}
Экраны для отображения и отслеживания текущего стека
@SuppressLint("RestrictedApi")
@Composable
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 by navController.currentBackStack.collectAsState()
val pagerState = rememberPagerState {
2
}
HorizontalPager(state = pagerState) { page ->
val headerText = if (page == 0) "Current Back stack(reversed)" else "Current hierarchy"
Column {
Text(
modifier = Modifier
.fillMaxWidth()
.padding(bottom = 8.dp),
text = headerText,
fontSize = 20.sp,
fontWeight = FontWeight.Bold
)
val destinations = if (page == 0) {
currentBackStack.reversed().map { it.destination }
} else {
navController.currentDestination?.hierarchy?.toList() ?: listOf()
}
LazyColumn(
modifier = Modifier.fillMaxSize(),
verticalArrangement = Arrangement.spacedBy(8.dp)
) {
items(items = destinations) { destination: NavDestination ->
if (destination is NavGraph) {
MainText(destination, packageName)
destination.nodes.forEach { _, value ->
SubItemText(value, packageName)
}
} else {
MainText(destination, packageName)
}
}
}
}
}
}
}
@Composable
private fun SubItemText(destination: NavDestination, packageName: String?) {
Row(
modifier = Modifier
.padding(start = 8.dp, bottom = 2.dp)
.shadow(2.dp, RoundedCornerShape(8.dp))
.background(Color.White)
.fillMaxWidth()
.padding(8.dp),
verticalAlignment = Alignment.Bottom
) {
Text(
text = getDestinationFormattedText(
destination,
packageName
),
fontSize = 12.sp
)
// destination.parent?.let {
// Text(
// text = ", parent: " + getGraphFormattedText(it, packageName),
// fontSize = 10.sp
// )
// }
}
}
@Composable
private fun MainText(destination: NavDestination, packageName: String?) {
Row(
modifier = Modifier
.shadow(4.dp, RoundedCornerShape(8.dp))
.background(Color.White)
.fillMaxWidth()
.padding(16.dp),
verticalAlignment = Alignment.Bottom
) {
Text(
text = getDestinationFormattedText(
destination,
packageName
),
fontSize = 18.sp
)
// destination.parent?.let {
// Text(
// text = ", parent: " + getGraphFormattedText(it, packageName),
// fontSize = 14.sp
// )
// }
}
}
@SuppressLint("RestrictedApi")
private fun getDestinationFormattedText(
destination: NavDestination,
packageName: String?,
) = (destination.route
?.replace("$packageName.", "")
?.replace("BottomNavigationRoute.", "")
?: (destination.displayName))
@SuppressLint("RestrictedApi")
private fun getGraphFormattedText(
destination: NavGraph,
packageName: String?,
) = (destination.route
?.replace("$packageName.", "")
?.replace("BottomNavigationRoute.", "")
?: (destination.displayName))
Маршруты и данные маршрутов
@Serializable
sealed class BottomNavigationRoute {
@Serializable
data object DashboardRoute : BottomNavigationRoute()
@Serializable
data object HomeGraph : BottomNavigationRoute()
@Serializable
data object HomeRoute1 : BottomNavigationRoute()
@Serializable
data object HomeRoute2 : BottomNavigationRoute()
@Serializable
data object HomeRoute3 : BottomNavigationRoute()
@Serializable
data object SettingsGraph : 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.HomeRoute1
),
BottomRouteData(
title = "Settings",
icon = Icons.Default.Settings,
route = BottomNavigationRoute.SettingsRoute1
),
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/platfor ... ationUI.kt
*/
internal tailrec fun findStartDestination(graph: NavDestination): NavDestination {
return if (graph is NavGraph) findStartDestination(graph.startDestination!!) else graph
}
Подробнее здесь: https://stackoverflow.com/questions/791 ... e-safety-i