How can I keep tab A selected when I navigate to N...
# compose-android
m
How can I keep tab A selected when I navigate to Nested Screen I'm currently using this to see if the current tab is selected or not based on the current destination (I haven't migrated to the new type-safe navigation APIs yet):
Copy code
val isSelected = currentDestination?.hierarchy?.any { it.route == tab.route } == true
but the problem is that it only works for top level destinations in my hierarchy (tab A, tab B, tab C). I think it is easy but I can't figure it out
s
This works if your graph looks like this:
Copy code
NavHost(route = "Root") {
 navigation("Tab A", startDestination = "Tab A screen") {
  composable("Tab A screen") {}
  composabel("NestedScreen") {}
 }
 navigation("Tab B") {}
 navigation("Tab C") {}
}
And so on.
hierarchy
looks up the graph, so it needs to find
Tab A
up the graph to match it. In `NestedScreen`'s hierarchy going up, it will first find "Tab A" and then "Root".
It does not work if you got this graph instead
Copy code
NavHost(route = "Root") {
 navigation("Tab A", startDestination = "Tab A screen") {
  composable("Tab A screen") {}
 }
 navigation("Tab B") {}
 navigation("Tab C") {}
 composable("NestedScreen") {}
}
Since for "NestedGraph", the tab you are trying to find in the hierarchy just isn't there. The only thing it will find going up the hierarchy is the "Root" route, the one your NavHost has.
m
Then it makes sense why it didn't work, Thanks Stylianos. Actually, my nav graph looks like this:
Copy code
NavHost(route = "Root") {
 composable("Tab A") {}
 composable("Tab B") {}
 composable("Tab C") {}
 composable("NestedScreen") {}
}
I'm trying to use your proposed solution but the problem is I can navigate to
NestedScreen
from tab A and tab B and I want to make whatever tab I navigate from is the selected one. I think I didn't clarify that in my original question
So, I can't make NestedScreen exclusive to tab A graph
s
You would still be able to navigate there no problem
If you want it to still highlight the tab you came from , you want 3 individual destinations.
i
If you aren't using the hierarchy to determine what tab you are on, then using the hierarchy for your logic isn't the right tool - you'll need to track that yourself separately
s
Copy code
NavHost(route = "Root") {
 navigation("Tab A", startDestination = "Tab A screen") {
  composable("NestedScreenA") { SameComposableForNestedScreen() }
 }
 navigation("Tab B") {
  composable("NestedScreenB") { SameComposableForNestedScreen() }
 }
 navigation("Tab C") {
  composable("NestedScreenC") { SameComposableForNestedScreen() }
 }
}
Is an easy way to do it. If that is what you want of course and you still want to rely on hierarchy to grab the selected item. Then depending on which tab you are in, navigate to the right route accordingly. If you want to have a deep link to them however, and you do not want the deep link to always end up in the "A" version of it or something like that, it can get a bit more involved. If you do not have a deep link this honestly works perfectly fine.
m
I don't want to the highlight the tab I came from just marking it as selected is fine for my case, and Thankfully I don't have to handle deeplinks for this use case as well. I tried your approach Stylianos but it is still the same result, here is a simple reproducer if you ever wanted to try it out:
Copy code
@Composable
fun Root(modifier: Modifier = Modifier) {
    val navController = rememberNavController()
    val navBackStackEntry by navController.currentBackStackEntryAsState()
    val currentDestination = navBackStackEntry?.destination

    val topLevelDestinations = listOf(
        Tab(
            "Tab A",
            Icons.Outlined.Home,
            createRoutePattern<NavigationDestination.A>(),
            NavigationDestination.A,
        ),
        Tab(
            "Tab B",
            Icons.Outlined.Add,
            createRoutePattern<NavigationDestination.B>(),
            NavigationDestination.B,
        ),
        Tab(
            "Tab C",
            Icons.Outlined.Settings,
            createRoutePattern<NavigationDestination.C>(),
            NavigationDestination.C,
        ),
    )
    Column(modifier = modifier) {
        Box(modifier = Modifier.weight(1f)) {
            NavHost(
                navController = navController,
                startDestination = "Tab B",
                modifier = Modifier.fillMaxSize(),
                route = "Root"
            ) {
                navigation(
                    startDestination = createRoutePattern<NavigationDestination.A>(),
                    route = "Tab A"
                ) {
                    composable<NavigationDestination.A> {
                        Box(
                            contentAlignment = Alignment.Center
                        ) {
                            Column(horizontalAlignment = Alignment.CenterHorizontally) {
                                Text(text = "Screen A")
                                Button(onClick = { navController.navigate(NavigationDestination.Nested) }) {
                                    Text(text = "Navigate to Nested screen")
                                }
                            }
                        }
                    }
                    composable<NavigationDestination.Nested> {
                        Box(
                            contentAlignment = Alignment.Center
                        ) {
                            Text(text = "Screen Nested")
                        }
                    }
                }

                navigation(
                    startDestination = createRoutePattern<NavigationDestination.B>(),
                    route = "Tab B"
                ) {
                    composable<NavigationDestination.B> {
                        Box(
                            contentAlignment = Alignment.Center
                        ) {
                            Column(horizontalAlignment = Alignment.CenterHorizontally) {
                                Text(text = "Screen B")
                                Button(onClick = { navController.navigate(NavigationDestination.Nested) }) {
                                    Text(text = "Navigate to Nested screen")
                                }
                            }
                        }
                    }
                }

                navigation(
                    startDestination = createRoutePattern<NavigationDestination.C>(),
                    route = "Tab C"
                ) {
                    composable<NavigationDestination.C> {
                        Box(
                            contentAlignment = Alignment.Center
                        ) {
                            Text(text = "Screen C")
                        }
                    }
                }
            }
        }
        NavigationBar(
            modifier = Modifier
                .fillMaxWidth()
        ) {
            topLevelDestinations.forEach { tab ->
                val isSelected = currentDestination?.hierarchy?.any { it.route == tab.route } == true
                NavigationBarItem(
                    selected = isSelected,
                    onClick = {
                        if (!isSelected) {
                            navController.navigate(tab.destination) {
                                popUpTo(navController.graph.findStartDestination().id) {
                                    saveState = true
                                }
                                launchSingleTop = true
                                restoreState = true
                            }
                        }
                    },
                    label = { Text(text = tab.label) },
                    icon = { Icon(tab.icon, contentDescription = null) }
                )
            }
        }
    }
}

data class Tab(
    val label: String,
    val icon: ImageVector,
    val route: String,
    val destination: NavigationDestination,
)
and destinations are (I'm using kiwi navigation compose typed):
Copy code
sealed interface NavigationDestination : Destination {

    @Serializable
    data object A : NavigationDestination

    @Serializable
    data object B : NavigationDestination

    @Serializable
    data object C : NavigationDestination

    @Serializable
    data object Nested : NavigationDestination
}
s
You are doing
val isSelected = currentDestination?.hierarchy?.any { it.route == tab.route } == true
but
tab.route
is
createRoutePattern<NavigationDestination.A>()
. The sub-nav-graph route is not that, it's
"Tab A"
, so you need to compare it with that instead.
✅ 1
I'd do
Copy code
sealed interface NavigationDestination : Destination {
    @Serializable
    data object AGraph : NavigationDestination
    @Serializable
    data object BGraph : NavigationDestination
    @Serializable
    data object CGraph : NavigationDestination

    @Serializable
    data object A : NavigationDestination

    @Serializable
    data object B : NavigationDestination

    @Serializable
    data object C : NavigationDestination

    @Serializable
    data object Nested : NavigationDestination
}
and then use the type-safety for the navgraph too
Copy code
navigation<NavigationDestination.AGraph>(
  startDestination = createRoutePattern<NavigationDestination.A>(),
) {
And probably
Copy code
val topLevelDestinations = listOf(
        Tab(
            "Tab A",
            Icons.Outlined.Home,
            createRoutePattern<NavigationDestination.AGraph>(),
            NavigationDestination.AGraph,
        ),
        Tab(
            "Tab B",
            Icons.Outlined.Add,
            createRoutePattern<NavigationDestination.BGraph>(),
            NavigationDestination.BGraph,
        ),
        Tab(
            "Tab C",
            Icons.Outlined.Settings,
            createRoutePattern<NavigationDestination.CGraph>(),
            NavigationDestination.CGraph,
        ),
    )
so that you do not get confuse about what is what. tl;dr: You need to differentiate between what's the graph's route and the destination's route and use the right one, you are trying to match the composable<> route here and not the one of the navigation<>
m
You're right Indeed, I tried comparing with the actual route and it works. Thanks
🌟 1
I think I might go with this sealed hierarchy as well
s
Btw regarding this, it's a bit out of topic, but I don't think it's a good idea in a real project. You probably want to have each tab be a separate feature module anyway, and you want that module to expose it's navgraph standalone without having to know about the other tabs necessarily. We do that, and we have a simple enum indicating the top level graphs like
Copy code
enum class TopLevelGraph {
  Home,
  Insurances,
  Payments,
  Profile,
}
And at the point where we want to check we just exhaustively go over this enum, https://github.com/HedvigInsurance/android/blob/afb510b3ebdf48ebc9e9597d31fc5de13d[…]/kotlin/com/hedvig/android/app/ui/IsTopLevelGraphInHierarchy.kt but we then map manually to each destination, which again, is not part of some sealed hierarchy for this. Same for when we want to navigate to them https://github.com/HedvigInsurance/android/blob/876c462a2e774d74637bbf18695017b785[…]app/src/main/kotlin/com/hedvig/android/app/ui/HedvigAppState.kt. Also with the way you got it now, since you got the sealed interface itself be a
Destination
inheritor, you can write
Copy code
navigation<NavigationDestination> {}
And that would not complain when you write it, but it is not what you want at all. Since you're also using the kiwi library (like we do too atm) I'd recommend just making the destinations just inherit from both
: NavigationDestination, Destination
to avoid this problem.
m
I totally agree with you, the example I gave is simplified of course to focus on this specific part (selecting the current tab). this is why I wrote all of navigation logic in one file. but, yeah in a real project I know about the best practice which is to split navigation into multiple sub graphs and have each one in a separate module. I know about this from official doc & from reading about it in previous threads from you & Ian. and thanks for the references I'll take a look at them.
💯 1