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!