I have an issue with the hierarchy of my Compose Navigation. The version of compose-navigation I am using is: androidx.navigation:navigation-compose:2.7.7
.
I am trying to implement simple navigation with bottom navigation and three tabs. Each tab hosts its own nested navigation, consisting of two screens: the main tab content and a details screen, which is reused across nested graphs.
Main Activity with NavHost for the Tabs:
class MainActivity : ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
enableEdgeToEdge()
setContent {
TestNavigationTheme {
val tabsController = rememberNavController()
Scaffold(
bottomBar = {
NavigationBar {
val navBackStackEntry by tabsController.currentBackStackEntryAsState()
val currentDestination = navBackStackEntry?.destination
BottomTabs.items.forEach { navigationItem ->
NavigationBarItem(
label = { Text(navigationItem.title) },
selected = currentDestination
?.hierarchy
?.any { it.route == navigationItem.route } == true,
icon = { Icon(navigationItem.icon, navigationItem.title) },
onClick = {
tabsController.navigate(navigationItem.route) {
popUpTo(tabsController.graph.findStartDestination().id) {
saveState = true
}
launchSingleTop = true
restoreState = true
}
}
)
}
}
}
) { paddingValues ->
NavHost(
navController = tabsController,
startDestination = BottomTabs.Feed.route,
modifier = Modifier.padding(paddingValues = paddingValues)
) {
navigation(
route = BottomTabs.Feed.route,
startDestination = "feed"
) {
composable(route = "feed") {
FeedScreen {
tabsController.navigate("details/feed")
}
}
details()
}
navigation(
route = BottomTabs.Search.route,
startDestination = "search"
) {
composable(route = "search") {
SearchScreen {
tabsController.navigate("details/search")
}
}
details()
}
navigation(
route = BottomTabs.Favourites.route,
startDestination = "fav"
) {
composable(route = "fav") {
FavouritesScreen {
tabsController.navigate("details/fav")
}
}
details()
}
}
}
}
}
}
private fun NavGraphBuilder.details() {
composable(
route = "details/{argument}",
arguments = listOf(
navArgument("argument") { type = NavType.StringType }
)
) { backStackEntry ->
DetailsScreen(
argument = backStackEntry
.arguments
?.getString("argument")
?: "Couldn't obtain the argument"
)
}
}
}
BottomTabs:
sealed class BottomTabs(val route: String, val title: String, val icon: ImageVector) {
data object Feed : BottomTabs("feed-tab", "Feed", Icons.Default.Home)
data object Search : BottomTabs("search-tab", "Search", Icons.Default.Search)
data object Favourites : BottomTabs("fav-tab", "Favourites", Icons.Default.Favorite)
companion object {
val items = listOf(Feed, Search, Favourites)
}
}
Tab Screens:
@Composable
fun FeedScreen(navigateToDetails: () -> Unit) {
Column(
verticalArrangement = Arrangement.Center,
horizontalAlignment = Alignment.CenterHorizontally,
modifier = Modifier.fillMaxSize()
) {
Text("Hi, I'm Feed screen")
Spacer(modifier = Modifier.size(40.dp))
Button(onClick = navigateToDetails) {
Text(text = "Go to details")
}
}
}
@Composable
fun SearchScreen(navigateToDetails: () -> Unit) {
Column(
verticalArrangement = Arrangement.Center,
horizontalAlignment = Alignment.CenterHorizontally,
modifier = Modifier.fillMaxSize()
) {
Text("Hi, I'm Search screen")
Spacer(modifier = Modifier.size(40.dp))
Button(onClick = navigateToDetails) {
Text(text = "Go to details")
}
}
}
@Composable
fun FavouritesScreen(navigateToDetails: () -> Unit) {
Column(
verticalArrangement = Arrangement.Center,
horizontalAlignment = Alignment.CenterHorizontally,
modifier = Modifier.fillMaxSize()
) {
Text("Hi, I'm Favourites screen")
Spacer(modifier = Modifier.size(40.dp))
Button(onClick = navigateToDetails) {
Text(text = "Go to details")
}
}
}
Details Screen:
@Composable
fun DetailsScreen(argument: String) {
Column(
verticalArrangement = Arrangement.Center,
horizontalAlignment = Alignment.CenterHorizontally,
modifier = Modifier.fillMaxSize()
) {
Text("I'm details screen")
Text("Your argument is: $argument")
}
}
You can find this code inside NavigationBarItem, which I took from Google's guide to navigation:
selected = currentDestination
?.hierarchy
?.any { it.route == navigationItem.route } == true
When I switch to the third tab ("fav-tab") and press a button to open the details screen, the details screen opens correctly within the fav-tab. However, the navigation bar highlights the feed-tab as selected instead of the fav-tab.
I tried debugging the navigation recomposition cycle by placing a breakpoint in NavigationBarItem. Here is the hierarchy:
I don't understand why the feed-tab
appears as the root of the hierarchy. In my opinion, it should be:
"details/{argument}"
"fav-tab"
because I navigated to the details screen from the fav-tab
, not the feed-tab
.
What am I doing wrong in my implementation? Any thoughts or advice would be greatly appreciated!
The cause of the issue was the route of the details screen. It was the same across all three navigation graphs.
According to the documentation, "Route uniquely identifies a destination and any data required by it."
So, I added prefixes to the details() composable, and now everything works fine.