Reputation: 4289
The app I'm building uses compose navigation with routes. The challenge is that the start destination is dynamic.
Here is a minimal example:
class MainActivity : ComponentActivity()
{
override fun onCreate(savedInstanceState: Bundle?)
{
super.onCreate(savedInstanceState)
setContent {
val navController = rememberNavController()
NavHost(
navController = navController,
startDestination = "dynamic/1", // doesn't work
// startDestination = "static", // workaround
) {
composable(
route = "dynamic/{$ARG_ID}",
arguments = listOf(navArgument(ARG_ID) { type = NavType.StringType }),
) {
val id = it.arguments?.getString(ARG_ID)
Text("dynamic route, received argument: $id!")
}
// part of the workaround
// composable(
// route = "static",
// ) {
// LaunchedEffect(this) {
// navController.navigate("dynamic/1")
// }
// }
}
}
}
companion object
{
const val ARG_ID = "id"
}
}
The app crashes with
java.lang.IllegalArgumentException: navigation destination route/1 is not a direct child of this NavGraph
The problem only exists if the "dynamic" route is used as start destination. This can be verified by using startDestination = "static"
.
Although, the "static" route workaround works I'm looking for a solution without it because it kind of obfuscates the code and also creates an additional entry in the back stack.
-> Full code sample to reproduce the issue
Related SO questions
Edit:
I want to stress that the original sample used to not contain the "static" composable. I only added the "static" composable to have a working startDestination
and to prove that the "dynamic" composable can be navigated to.
Update:
Even switching to the query parameter syntax for optional arguments, providing a default value, and setting the start destination without any argument does not work.
The following variation
NavHost(
navController = navController,
startDestination = "dynamic",
) {
composable(
route = "dynamic?$ARG_ID={$ARG_ID}",
arguments = listOf(navArgument(ARG_ID) { type = NavType.StringType; defaultValue = "1" }),
) {
val id = it.arguments?.getString(ARG_ID)
Text("dynamic route, received argument: $id!")
}
}
Leads to the exception
java.lang.IllegalArgumentException: navigation destination dynamic is not a direct child of this NavGraph
Upvotes: 36
Views: 15360
Reputation: 19
If your first screen requires dynamic data fetched from an api or something like that, the default value won't do it.
It's not the cleanest solution. But you can compose an empty dummy screen first, after it loads you can navigate to the actual home screen
var isPlaced by remember { mutableStateOf(false) }
LaunchedEffect(isPlaced) {
if (isPlaced) {
navController.navigate("home/some-argument") {
launchSingleTop = true
}
}
}
NavHost(
navController = navController,
startDestination = "start"
) {
composable("start") {
Box(
modifier = Modifier.onGloballyPositioned {
isPlaced = true
}
) {}
}
composable(
"home/{argument}",
) { backStackEntry ->
backStackEntry.arguments?.getString(argument)?.let {
HomeScreen(it)
}
}
}
Upvotes: 1
Reputation: 163
You can follow this official documentation
[https://developer.android.com/jetpack/compose/navigation][1]
composable(
"profile?userId={userId}",
arguments = listOf(navArgument("userId") { defaultValue = "user1234" })
) { backStackEntry ->
Profile(navController, backStackEntry.arguments?.getString("userId"))
}
But it has only one limitation you can't pass boolean in args for eg if you replace userId with any boolean value it does not work when you use it in start destination.
Upvotes: 0
Reputation: 2725
What I did was explicitly navigate again in case start destination has params.
This way we also have to pop up the stack to remove initial navigation to start destination without params.
This solution might not work in all scenarios, but was enough to get me out of the situation.
LaunchedEffect(key1 = Unit) {
delay(500)
if (doesStartDestinationHaveArgs(startDestination)) {
navController.popBackStack()
navController.navigate(startDestination)
}
}
Upvotes: -1
Reputation: 480
All credit goes to ianhanniballake and Peter. In my case I didn't add any additional (mandatory key/optional key) in route for the argument data. I kept the route clean like below:
Nav graph:
navigation(route = Route.Root.route, startDestination = Route.SubmitForm.route) {
composable(
route = Route.SubmitForm.route,
arguments = listOf(
navArgument(ARG_KEY) {
type = NavType.StringType
defaultValue = JsonConverter.toJson(user, User::class.java)
},
)
)
}
Route sealed class:
sealed class Route(val route: String) {
object MyRoute : Route("$ROOT/submit-form")
}
And in view model just get the data like this:
@HiltViewModel
class MyViewModel @Inject constructor(
stateHandle: SavedStateHandle,
) : ViewModel {
lateinit var user
init {
user = stateHandle.get<String>(ARG_NAME) // Supported data types
}
}
It worked for me.
Upvotes: 1
Reputation: 439
should not be using dynamic route value in "startDestination" NavHost --> navController.navigate(<dynamic route >)
Upvotes: 0
Reputation: 4289
Full credit goes to ianhanniballake, who explained the solution to me in a comment. I'm going to show the working version of my code sample here.
The big insight to me was:
startDestination
must not match a composable route
in the sense of pattern matching but it must be exactly the same string.
That means an argument can't be set via startDestination
directly but has to be set via the argument's defaultValue
.
Here is the working sample:
class MainActivity : ComponentActivity()
{
override fun onCreate(savedInstanceState: Bundle?)
{
super.onCreate(savedInstanceState)
setContent {
val navController = rememberNavController()
NavHost(
navController = navController,
// 1st change: Set startDestination to the exact string of route
startDestination = "dynamic/{$ARG_ID}", // NOT "dynamic/1", provide arguments via defaultValue
) {
composable(
route = "dynamic/{$ARG_ID}",
// 2nd change: Set startDestination argument via defaultValue
arguments = listOf(navArgument(ARG_ID) { type = NavType.StringType; defaultValue = "1" }),
) {
val id = it.arguments?.getString(ARG_ID)
Text("dynamic route, received argument: $id!")
}
}
}
}
companion object
{
const val ARG_ID = "id"
}
}
The approach equally works with the argument provided in the form of a query parameter.
To be honest, I see this as a small limitation because the start route now dictates what has to be the defaultValue
. I might want to set a different defaultValue
or none at all. Yet, in most cases this should be the most elegant solution.
Upvotes: 54