Hi all, So I'm working with the new Compose TypeS...
# compose-android
m
Hi all, So I'm working with the new Compose TypeSafe navigation with Bottom NavigationBar. I'm facing a few issues. 1. Clicking on the back button navigates to the previous screen but the NavigationBar Item is not updated. How can I do that? e.g. When I navigate Series Screen -> Favourite Screen, my understanding is that back button press should navigate Series Screen and I also want the bottom bar item to updated. I can do this with routes but not with Compose TypeSafe Navigation. 1. Let's say I navigated to the Details screen on the Home tab, Home -> Details Screen. When I navigate back to Home item from any other item, the start destination opens instead of the Details Screen. But if I navigate by clicking on the Home Item, the Details Screen oepns. You can see the video below for a demonstration. How can I resolve this issue? Thanks
🟢 1
Screen Recording 2024-07-12 at 3.38.55 PM.mov
s
How are you determining which item is highlighted? You want the source of truth from that to come from your NavController. Perhaps take a look at this https://kotlinlang.slack.com/archives/C0B8M7BUY/p1719960968535549?thread_ts=1719960839.952169&cid=C0B8M7BUY Or https://cs.android.com/androidx/platform/frameworks/support/+/androidx-main:navigation/navigation-compose/integration-tests/navigation-demos/src/main/java/androidx/navigation/compose/demos/BottomBarNavDemo.kt;l=64 For the nested nav question, follow what they say here to support nested nav stacks https://developer.android.com/develop/ui/compose/navigation#bottom-nav focus on the snippet where they do the restoreState and saveState while navigating between the top level graphs
☝️ 1
🙌 1
🙏 1
m
This is how I'm determining the selected item.
Copy code
NavigationBar {
    var selectedItemIndex by remember { mutableIntStateOf(0) }
    navigationItems.forEachIndexed { index, item ->
        NavigationBarItem(
            label = { Text(text = item.title) },
            selected = index == selectedItemIndex,
            onClick = {
                selectedItemIndex = index
                onItemClick(item.navRoute)
            },
            icon = {
                Icon(
                    if (index == selectedItemIndex) item.selectedIcon else item.unSelectedIcon,
                    contentDescription = null
                )
            })
    }
}
By keeping the selected item index in the state. But I'm unable to update that from the navBackStackEntry. I also did this.
Copy code
val navBackStackEntry by rootNavController.currentBackStackEntryAsState()
val currentRoute = remember(navBackStackEntry) {
    GraphRoute.fromRoute(navBackStackEntry?.destination?.route ?: "")
}
But this also doesn't work.
s
var selectedItemIndex by remember { mutableIntStateOf(0) }
There you go, you do not derive the bottom tab selected from the NavController's current destination, but you got something custom. Follow the linked thread above or just go to https://cs.android.com/androidx/platform/frameworks/support/+/androidx-main:navigati[…]va/androidx/navigation/compose/demos/BottomBarNavDemo.kt;l=64 to get a real impl. This has nothing to do with the new type-safe APIs or not using them. To be able to check the route with the new type-safe APIs use this https://cs.android.com/androidx/platform/frameworks/support/+/androidx-main:navigati[…]=815?q=hasRoute&ss=androidx%2Fplatform%2Fframeworks%2Fsupport as I linked above too.
1
🙏 1
m
@Stylianos Gakis I got it working like this.
Copy code
val navBackStackEntry by rootNavController.currentBackStackEntryAsState()
val currentRoute = GraphRoute.fromRoute(navBackStackEntry?.destination?.parent?.route ?: "")
As each BottomBar tab have it's own graph. I'm getting the parent of the current destination and mapping that to GraphRoutes. I also implemented this helper method to convert the TyepSafe route, which is a string, to Route model class.
Copy code
companion object {
    fun fromRoute(route: String): GraphRoute? {
        return GraphRoute::class.sealedSubclasses.firstOrNull {
            route.contains(it.qualifiedName.toString())
        }?.objectInstance
    }
}
Thanks for the help.
I've tried the other solution that you mentioned (
hasRoute
) and that one also works. Don't know which one to use but I'll check if any of them cause unnecessary recompositions. Thanks
s
Glad to hear that! You probably want the hasRoute so that it properly checks the type-safe route rather than having to do a toString on the qualifiedName of your enum yourself. I wouldn't trust that at all over just calling the right apis which were made for this particular thing directly.
m
Ok, got it. One last thing, will this by any means, cause extra recomposition?? Sorry if this is a dumb question but I'm new to compose navigation.
s
Don't worry about these things before they have become a problem. It really isn't something you should spend your time on imo. If you want, open the layout inspector and see how often it recomposes.
currentBackStackEntryAsState
will of course change when you navigate, you will get a recomposition there, and the new state will be reflected in the UI. This is just compose working as intended, nothing to be worried of there.
m
Premature optimization is the root of all evil.
Totally makes sense, I think
currentBackStackEntryAsState
gets triggered for the nested destination changes as well, and that causes the recompositions. But not a problem at this stage. Will try this with multiple backstacks as well.
s
Yes that will change, the
Copy code
selected = currentDestination?.hierarchy?.any { it.hasRoute(destination::class) } == true,
will run again, but
selected
will be the same as it was before. So the
BottomNavigationItem
will not need to recompose itself, since it's gonna be equal to what it was before. I wouldn't worry about this at all unless you can trace that this is somehow actually slowing your app down.
1
m
I wouldn't worry about this at all unless you can trace that this is somehow actually slowing your app down.
Totally agree, nothing like that happening. One last question, do I need to make the selected a composeState??
This is my NavigationBar Composable.
Copy code
@Composable
fun BottomNavBar(
    currentDestination: NavDestination?,
    onItemClick: (GraphRoute) -> Unit,
) {

    val navigationItems = listOf(
        BottomNavBarItem(
            false, "Movies", Icons.Filled.Movie, Icons.Outlined.Movie, GraphRoute.MovieList
        ),
        BottomNavBarItem(
            false,
            "Series",
            Icons.Filled.MovieFilter,
            Icons.Outlined.MovieFilter,
            GraphRoute.SeriesList
        ),
        BottomNavBarItem(
            false,
            "Favourites",
            Icons.Filled.Favorite,
            Icons.Filled.FavoriteBorder,
            GraphRoute.Favourites
        )
    )

    NavigationBar {
        navigationItems.forEachIndexed { index, item ->
            
            val isSelected = currentDestination?.hierarchy?.any {
                it.hasRoute(item.navRoute::class)
            } == true
            
            NavigationBarItem(
                label = { Text(text = item.title) },
                selected = isSelected,
                onClick = { onItemClick(item.navRoute) },
                icon = {
                    Icon(
                        if (isSelected) item.selectedIcon else item.unSelectedIcon,
                        contentDescription = null
                    )
                })
        }
    }
}
s
If anything, move your
navigationItems
as a private local value in that file instead so that you do not re-create this list every time you get a new currentDestination. That list seems to never change anyway so it can stay there.
💡 1
m
Moved the
navigationItems
to the top level outside the composables.
162 Views