0

I'm using Jetpack Compose and androidx-navigation-compose.

Generally, I like the save/restore state mechanism when switching between bottom navigation tabs.

However, if I'm in a detail screen of a particular tab and I click on the tab again I wnat the app to navigate to the respective top level destination of that tab - opposed to doing nothing. How can I achieve this behavior?

The behavior I described is not implemented in Now-in-Android. However, it used to be the default behavior when setting up a Bottom Bar with androidx navigation - before Compose.

I tried to built a minimal example, which can be found here.

Here is the most relevant part:

Scaffold(
    modifier = Modifier.fillMaxSize(),
    bottomBar = {
        NavigationBar {
            bottomBarItems.forEach { item ->
                val screen = item.screen
                NavigationBarItem(
                    icon = { Icon(Icons.Filled.Favorite, contentDescription = null) },
                    label = { Text(item.title) },
                    selected = screen == currentTopLevelDestination.screen,
                    onClick = {
                        navController.navigate(screen.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
                        }
                    }
                )
            }
        }

    }
) { paddingValues ->
    NavHost(
        navController = navController,
        startDestination = TopLevelDestination.HOME.route,
        modifier = Modifier.padding(paddingValues)
    ) {
        navigation(
            route = TopLevelDestination.HOME.route,
            startDestination = Screen.Home.route,
        ) {
            composable(Screen.Home.route) {
                Box(
                    contentAlignment = Alignment.Center,
                    modifier = Modifier.fillMaxSize(),
                ) {
                    Button(onClick = { navController.navigate(Screen.HomeDetail.route) }) {
                        Text("Go to Home Detail")
                    }
                }
            }
            composable(Screen.HomeDetail.route) {
                Box(
                    contentAlignment = Alignment.Center,
                    modifier = Modifier.fillMaxSize(),
                ) {
                    Text("Home Detail")
                }
            }
        }
        navigation(
            route = TopLevelDestination.MORE.route,
            startDestination = Screen.More.route,
        ) {
            composable(Screen.More.route) {
                Box(
                    contentAlignment = Alignment.Center,
                    modifier = Modifier.fillMaxSize(),
                ) {
                    Button(onClick = { navController.navigate(Screen.MoreDetail.route) }) {
                        Text("Go to More Detail")
                    }
                }
            }
            composable(Screen.MoreDetail.route) {
                Box(
                    contentAlignment = Alignment.Center,
                    modifier = Modifier.fillMaxSize(),
                ) {
                    Text("More Detail")
                }
            }
        }
    }
}

What I want is the following behavior:

  1. App starts in Home screen
  2. Navigate to Home Detail
  3. Tap on Home bottom navigation item

-> I want to go back to Home

Peter F
  • 3,633
  • 3
  • 33
  • 45
  • This is one of the reasons we should avoid showing navigation bars in detail screens. It may lead to various navigation issues and also be misleading to user. Use a back icon to allow returning to previous / dashboard screen. Play around with some popular apps - you will observe that the nav bar is almost never shown on detail screen. – Derek K Jul 20 '23 at 17:03
  • @DerekK Gmail, Spotify, and Now in Android for example do use navigation bars on details screens. Conceptually, I don't see a problem with it. The behavior I'm describing used to be the default behavior before compose navigation. – Peter F Jul 22 '23 at 07:36
  • Having a navigation bar on detail screens adds complexity, for sure, but it also brings the benefit of quicker navigation across the app. – Peter F Jul 22 '23 at 07:42

1 Answers1

0

Finally, I got this working in the GitHub sample

The initial idea to keep track of the currently selected bottom navigation tab was correct. It works if the navigation stack is not saved & restored if an already active tab is clicked.

Here is the most relevant code

@Composable
internal fun AppNavigationBar(
    navController: NavHostController
) {
    val currentTopLevelDestination by navController.currentTabItemAsState()

    NavigationBar {
        bottomBarItems.forEach { item ->
            val isTabAlreadySelected = item == currentTopLevelDestination
            NavigationBarItem(
                icon = { Icon(Icons.Filled.Favorite, contentDescription = null) },
                label = { Text(item.title) },
                selected = isTabAlreadySelected,
                onClick = {
                    navController.navigateToTabItem(
                        item = item,
                        restoreTabStack = !isTabAlreadySelected,
                    )
                }
            )
        }
    }
}

/**
 * Adds an [NavController.OnDestinationChangedListener] to this [NavController] and updates the
 * returned [State] which is updated as the destination changes.
 */
@Composable
private fun NavController.currentTabItemAsState(): State<TabItem> {
    val selectedItem = remember { mutableStateOf(TabItem.Home) }

    DisposableEffect(this) {
        val listener = NavController.OnDestinationChangedListener { _, destination, _ ->
            when {
                destination.hierarchy.any { it.route == TabItem.More.navGraphRoute } -> {
                    selectedItem.value = TabItem.More
                }

                // TopLevelDestination.HOME is the start destination and, therefore, part of any stack
                else -> {
                    selectedItem.value = TabItem.Home
                }
            }
        }
        addOnDestinationChangedListener(listener)

        onDispose {
            removeOnDestinationChangedListener(listener)
        }
    }

    return selectedItem
}

private fun NavHostController.navigateToTabItem(
    item: TabItem,
    restoreTabStack: Boolean
) {
    navigate(item.navGraphRoute) {
        // 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(graph.findStartDestination().id) {
            saveState = restoreTabStack
        }
        // Avoid multiple copies of the same destination when
        // reselecting the same item
        launchSingleTop = true
        // Restore state when reselecting a previously selected item
        restoreState = restoreTabStack
    }
}
Peter F
  • 3,633
  • 3
  • 33
  • 45