Pass an argument to a nested navigation graph in Jetpack Compose
Asked Answered
P

3

11

From the docs, I see you can nest navigation graphs like so:

NavHost(navController, startDestination = "home") {
    ...
    // Navigating to the graph via its route ('login') automatically
    // navigates to the graph's start destination - 'username'
    // therefore encapsulating the graph's internal routing logic
    navigation(startDestination = "username", route = "login") {
        composable("username") { ... }
        composable("password") { ... }
        composable("registration") { ... }
    }
    ...
}

I am wondering, how would one pass an argument in the route, and make that available to all composables inside the nav graph?

Here's my current nav graph:

navigation(
    // I'd like to grab this parameter
    route = "dashboard?classId={classId}",
    startDestination = Route.ScreenOne.route) {
    composable(Route.ScreenOne.route) {
        // And then pass the parameter here, or to any composable below
        ScreenOne(classId)
    }
    composable(Route.ScreenTwo.route) {
        ScreenTwo()
    }
    composable(Route.ScreenThree.route) {
        ScreenThree()
    }
}

I am basically trying to avoid setting the classId navigation argument individually on each composable route. I didn't see a way to pass a list of arguments to navigation() like you can in a composable().

It might be that what I am describing isn't possible, but looking forward to anyone's thoughts!

Padraic answered 23/11, 2021 at 18:32 Comment(1)
Hi I think I ran on the same problem as yours and if I understand you correctly, there might be a good chance that we were just over complicating things. You can access argument from your nested graph route in your startDestination route given they have the same name. And for non-startDestination routes, just pass them a their own argument, this will make them more independent.Mayweed
C
7

UPDATE: 6th April 2023 - Previously my answer was broken. I've now updated this to get the back stack entry from the nav controller. This solution now works.


Yes, this is perfectly possible - took me a while as the documentation is lacking (they explain Navigation arguments, and Nested Graphs, but not both!).

You will need access to your navController to do this.

You call getBackStackEntry on the navController to get the top most entry. It will need wrapping inside a remember(entry) { } block as you are calling it inside a Composable.

private fun NavGraphBuilder.yourNestedGraph(navController: NavHostController) {
    navigation(
        route = "dashboard?classId={classId}",
        startDestination = "yourStartDestination",
        arguments = listOf(navArgument("classId") { type = NavType.StringType })
    ) {
        composable(route = "yourStartDestination") { entry ->
            val parentEntry = remember(entry) { navController.getBackStackEntry("dashboard?classId={classId}") }
            val classId = parentEntry.arguments?.getString("classId")
            YourScreenComposable(classId)
        }
        composable(route = "someOtherNestedRoute") { entry ->
            val parentEntry = remember(entry) { navController.getBackStackEntry("dashboard?classId={classId}") }
            val classId = parentEntry.arguments?.getString("classId")
            AnotherScreen(classId)
        }
        composable(route = "andAnotherNestedRoute") { entry ->
            val parentEntry = remember(entry) { navController.getBackStackEntry("dashboard?classId={classId}") }
            val classId = parentEntry.arguments?.getString("classId")
            AndAnotherScreen(classId)
        }
    }
}

** Important Note ** - You may need to type the navigation function again and import it - similar to the issue mentioned in the question, I also couldn't see an arguments parameter at first in my existing navigation functions, but when I typed out navigation again inside my NavGraphBuilder I saw the other option which did have the arguments parameter.

Cromer answered 6/3, 2023 at 8:19 Comment(8)
I've tried to reproduce this, and only the first destination has access to it. In your example I suppose you navigate to the other destinatation inside the graph with navigate("someOtherNestedRoute") without passing argsStipel
Yes you're right I discovered this later in the day. Let me have another play then update the answer (or delete if it can't be done!)Cromer
Let me know if you find a solution. If not.. it would mean the other answer is wrong as wellStipel
Any update yet?Giffin
Got it working now @Stipel if you want to take a lookCromer
Also @Giffin if you want to lookCromer
You wrote: "but when I typed out navigation again". Could you please share import statement and the navigation library version you use? I don't see any navigation() which takes "arguments" parameterCarafe
I found. To have correct navigation() with arguments use "import androidx.navigation.compose.navigation" instead of "import androidx.navigation.navigation" checked on navigation-compose v2.6.0Carafe
S
2

You can access the graph arguments from child composables:

navController.getBackStackEntry("dashboard?classId={classId}").arguments?.getString("classId")
Subtropical answered 24/11, 2021 at 3:56 Comment(0)
O
0

From a quick test, it looks like passing arguments to a navigation graph is possible, but results in only the start destination receiving those arguments. For example, the following code does not work as a IllegalStateException: Required value was null crash would occur when instantiating a SecondScreenViewModel:

// Set up graph with arguments only applied to graph component
private fun NavGraphBuilder.testGraph(navController: NavHostController) {
    navigation(
        startDestination = "first",
        route = "graph/{argument}",
        arguments = listOf(navArgument("argument") { type = NavType.StringType })
    ) {
        composable(route = "first") {
            FirstScreen(navigateToSecondScreen = { navController.navigate("second") })
        }
        composable(route = "second") {
            SecondScreen()
        }
    }
}

// Screens implementing hilt injected view models
@Composable
fun FirstScreen(viewModel: FirstScreenViewModel = hiltViewModel()){ 
/**/ 
}
@Composable
fun SecondScreen(viewModel: SecondScreenViewModel = hiltViewModel()) {
 /**/ 
}

// Hilt view models giving access to the savedStateHandle
@HiltViewModel
class FirstScreenViewModel @Inject constructor(
    savedStateHandle: SavedStateHandle
): ViewModel() {
    // Works as expected
    private val argument: String = checkNotNull(savedStateHandle["argument"])
    //...
}
@HiltViewModel
class SecondScreenViewModel @Inject constructor(
    savedStateHandle: SavedStateHandle
): ViewModel() {
    // (CRASH OCCURS DUE TO ILLEGAL STATE EXCEPTION)
    private val argument: String = checkNotNull(savedStateHandle["argument"])
    //...
}

Instead, it looks like you can either go down the route others have suggested; accessing the arguments in the navigation back stack...

// BackStackEntry approach
private fun NavGraphBuilder.testGraph(navController: NavHostController) {
    navigation(
        startDestination = "first",
        route = "graph/{argument}",
        arguments = listOf(navArgument("argument") { type = NavType.StringType })
    ) {
        composable(route = "first") {
            FirstScreen(
                navigateToSecondScreen = {
                    val argument = navController.getBackStackEntry("graph/{argument}").arguments?.getString("argument")
                    navController.navigate("second/$argument")
                }
            )
        }
        composable(
            route = "second/{argument}",
            arguments = listOf(navArgument("argument") { type = NavType.StringType })
        ) {
            SecondScreen()
        }
    }
}

...or, you could always pass the argument between destinations using callbacks...

// Callback approach
private fun NavGraphBuilder.testGraph(navController: NavHostController) {
    navigation(
        startDestination = "first",
        route = "graph/{argument}",
        arguments = listOf(navArgument("argument") { type = NavType.StringType })
    ) {
        composable(route = "first") {
            FirstScreen(
                navigateToSecondScreen = { argument -> 
                    navController.navigate("second/$argument")
                }
            )
        }
        composable(
            route = "second/{argument}",
            arguments = listOf(navArgument("argument") { type = NavType.StringType })
        ) {
            SecondScreen()
        }
    }
}

Either way, if you want to access a navigation argument from within a child Composable's view model, it currently seems necessary to define the argument on each composable child within the navigation graph, with the exception of the start destination.

This feels a little strange to me - we can only hope for some sort go navigation-graph scoped argument in the future!

Ohl answered 14/2, 2023 at 2:25 Comment(2)
I'm trying to do this, but only the startDestination of the nested graph has access to it. Did you validate a second destination had access to the arguments?Stipel
Ah yes you were right - I jumped the gun with my initial answer it would seem! I have updated it to reflect your findings and hopefully provide some more insight on other methods. Thanks!Ohl

© 2022 - 2024 — McMap. All rights reserved.