with the `navigation` component, is there a way to...
# compose
b
with the
navigation
component, is there a way to know the size of the backstack? I’m setting up a
TopAppBar
(via
Scaffold
) and I’d like to show a back arrow if the current destination is not at the top of the back stack. I’m essentially trying to do something like
navController.backstack.size > 0
but the
backstatck
property access is restricted to the navigation lib itself. How can I determine the backstack size to know if I should show a back icon or not?
i
This is why the start destination is so special - you shouldn't show the up button when you're on the start destination of your graph. Otherwise, you should show it (and hook it up to
navController.navigateUp()
)
b
That’s fair. I suppose it could get a bit more complicated than that, for example if using a bottom nav bar and you don’t want to show an up button for the “primary” destinations for each item in the bar, but only when you navigate down from one of those. But I guess that’s easy enough to handle as well, particularly if you use the sealed class approach shown in the docs. You can add an
isTopLevelDestination
property on the sealed class and use it to determine whether or not to show an up button.
i
Yep. The important part is that the size of the back stack isn't actually what you want since the back stack size will be just 1 (i.e., you're on the last spot on the back stack) when your app is launched on another app's task stack via a deep link. That case is why
navigateUp()
and
popBackStack()
are different things -
navigateUp()
does the right thing
b
Makes sense. I managed to get the above mentioned scenario working, though it was a bit harder than I thought. Mainly when it comes to getting the instance of
Screen
that goes with a
route
. I had to use reflection to do it.
Copy code
sealed class Screen(val route: String, val title: String, val isTopLevel: Boolean = false) {
    object Home: Screen("home", "Home", true)
    object Post: Screen("post/{postId}", "Blog Post")
    object Settings: Screen("settings", "Settings", true)

    companion object {
        // create a map of routes to the Screen's they go with.
        // Uses reflection (uggh), so build the map once and keep it here,
        // instead of building it every time we need to look up a screen from a route.
        private val routeMap = Screen::class
            .nestedClasses
            .mapNotNull { it.objectInstance as? Screen }
            .associateBy { it.route }


        fun fromRoute(route: String?): Screen? {
            return routeMap[route]
        }
    }
}
With that, I can get the
Screen
from the current
route
and setup the Up Button Accordingly in my Activity:
Copy code
setContent {
            val navController = rememberNavController()
            KsbTheme {
                Scaffold(
                    topBar = {
                        val destination by navController.currentBackStackEntryAsState()
                        val route = destination?.arguments?.getString(KEY_ROUTE)
                        val isTopLevel = Screen.fromRoute(route)?.isTopLevel ?: true
                        AppBar(title = "My App", !isTopLevel)
                    }
                ) {
                    NavHost(navController = navController, startDestination = Screen.Home.route) {
                        ...
                    }
                }
            }
        }
i
That's a roundabout way to do it, but sure. You might look at something like an argument as part of your graph rather than round tripping through the route: https://stackoverflow.com/a/57928305/1676363
Copy code
setContent {
    val navController = rememberNavController()
    KsbTheme {
        Scaffold(
            topBar = {
                val destination by navController.currentBackStackEntryAsState()
                // Use arguments to determine whether this is a top level destination, defaulting to false
                val isTopLevel = destination?.arguments?.getBoolean("topLevel", false)
                AppBar(title = "My App", !isTopLevel)
            }
        ) {
            NavHost(navController = navController, startDestination = Screen.Home.route) {
                composable(
                    Screen.Home.route,
                    arguments = listOf(navArgument("topLevel") { defaultValue = true })
                ) {
                    ...
                }
                ...
            }
        }
    }
}
👍 1
a
hi @Ian Lake, I guess it makes sense if I could pass in defaults in arguments but doing it for `AppBar`'s title seems to be a problem. For example the title to be displayed would definitely come from
stringResource(...)
.. which couldn't be called or defined as a default value as its a
@Composable
function. Because of this the burden of defining the `AppBar`'s title falls under the previous screen calling the navigation. Also if I passed the value as a
navArgument(..)
is it possible to change its value? Lets say I passed in
topLevel = true
to show the back button but I would like to hide it (
topLevel = false
) if for example a network request is ongoing?
b
@Archie if your title's are String resources, I think you could setup the argument to be an Int (and set it to the appropriate string resource, and then when setting up the app bar, you can get the string using the
stringResource
Composable
a
@Bradleycorn, Yes thats true. But not all titles would be defined in the
stringResource()
some title would be defined as a string itself if it came from other source (for example the title should be the name of a selected item in a list). So then you'll have to check two sources of title.. one from an
StringRes
and another a
String
.
b
Yep ... so one way I've handled that (which admittedly is maybe not great) is to actually have two arguments, a with a String that contains the "dynamic" part of the title, and the other that is an Int that contains a string resource with the "static" part of the title. Then when setting up the app bar, you can get both and use a string resource with placeholders. And it may be useful to define a string resource that has ONLY a placeholder, like
<string name="placeholder">%1$s</string>
. That can be useful if you have some titles that are purely dynamic.
a
Its very workaroundy in my opinion :(
b
I don't disagree. IMO the problem is really that there's not a way to get a string from a string resource outside of a Composable scope. I (mostly) understand why
stringResource()
is a composable function, but it would be nice if there was a way to get a string from a resource without needing an
@Composable
at all (much like in the old view system, it would be nice to be able to get a string from a resource in a viewmodel without needing a context).
👍 1