I have created my NavGraph using the Kotlin DSL and everything is fine. But I'm struggling to pass a simple argument between destinations.
I'm folowing this Android Docs without success: https://developer.android.com/guide/navigation/navigation-kotlin-dsl#constants
Part of graph that adds the argument as the docs says:
fragment<RestaurantsTabsFragment>(
"${CampusSelectorDestinations.restaurantsTabsFragment}/" +
CampusSelectorArguments.campusId
) {
argument(CampusSelectorArguments.campusId) {
type = NavType.StringType
defaultValue = "test"
}
}
Code with the navigation action trying to pass a argument:
campusesAdapter.onCampusClick = { campusId ->
findNavController().navigate("${CampusSelectorDestinations.restaurantsTabsFragment}/" + campusId
}
Error I get:
IllegalArgumentException: Navigation destination that matches request NavDeepLinkRequest{ uri=android-app://androidx.navigation/restaurantsTabsFragment/jCkuLbzRHtW0CUzDFWYw } cannot be found in the navigation graph NavGraph
Can anyone help me? I can provide more information if needed
The pattern to pass the argumet route is wrong at the docs:
For luck, I've found this explanation inside a Navigation Lib class and that solved my problem (after 2 days struggling):
...
In addition to a direct Uri match, the following features are supported:
Uris without a scheme are assumed as http and https. For example, www.example.com will match http://www.example.com and https://www.example.com. Placeholders in the form of {placeholder_name} matches 1 or more characters. The String value of the placeholder will be available in the arguments Bundle with a key of the same name. For example, http://www.example.com/users/{id} will match http://www.example.com/users/4. The .* wildcard can be used to match 0 or more characters.
These Uris can be declared in your navigation XML files by adding one or more elements as a child to your destination.
...
Hope someone from Google see this and fixes the docs. (or explain if I'm wrong)
Just put your arguments into curved breaks and separate them by slash as it shown in the example below.
Define your destination with all required argument:
fragment<TransactionFragment>("${MainNavRoute.transaction}/{arg1}/{arg2}") {
argument("arg1") {
type = NavType.StringType
}
argument("arg2") {
type = NavType.LongType
}
Navigation to the destination:
findNavController().navigate("${MainNavRoute.transaction}/string_value/2")
Also, I have reported an issue to the tracker too. https://issuetracker.google.com/issues/221895357
Related
When working with optional arguments in Compose: What's the value between the braces?
"item=" is the key. What is the purpose of the string within the braces?
composable("Details?item={item}",
arguments = listOf(navArgument("item") {
this.type = NavType.StringType
defaultValue = "Item not available!"
})) {
You have it backwards - the name of the query parameter (the item= part) could be anything. It could be lorem_ipsum or foobar or any word - that word isn't used by Navigation at all for figuring out how to parse that argument. The only thing that matters is the structure of your route pattern - that query parameter syntax is how you mark a parameter as an optional argument. Just like a web URL, you could navigate to Details as well as Details?item=cat - both would match your route. A required parameter would be part of the path (i.e., Details/{item}).
As explained in the Navigate with arguments guide:
Navigation compose also supports passing arguments between composable destinations. In order to do this, you need to add argument placeholders to your route, similar to how you add arguments to a deep link when using the base navigation library
That {item} is the placeholder that indicates that whatever string is in the place of the {item} should be parsed and put as an argument with the name item. This is how you can call navBackStackEntry.arguments?.getString("item") and get that parsed value out of the arguments of the destination.
I've created multiple graphs in separate files.
The first one is GraphA and it's includes GraphB. the startDestination of GraphB is FragmentB that has an argument named AnID
Now I want to pass the AnID from GraphA to GraphB (FragmentB)
Despite of the editor know about the argument but generated codes don't regard the argument:
val directionB = FragmentADirections.actionFragmentAToGraphB(/* HAS NOT ARGUMENT */)
findNavController().navigate(directionB)
How can I pass the argument to a nested graph?
I found this but it's not an official solution:
val direction = FragmentADirections.actionFragmentAToGraphB()
findNavController().navigate(direction.actionId, FragmentBArgs(anId).toBundle())
Update:
someone introduced another way by defining nested-graph arguments in action
I've come across an interesting problem with trying to accomplish dynamic or conditional navigation with the Jetpack Navigation library.
The goal I have in mind is to be able to continue using the nav_graph.xml to manage the overall navigation graph, but simultaneously allow for conditional navigation based on some factors.
I have included some code below that shows where my solution is headed. The problem is that it inherently requires a lot of maintenance for future conditional logic to work.
I really want the navigateToDashboard function in the example to be able to be executed with either no parameters, or parameters that rarely change. For instance, instead of passing NavDirections, maybe passing some identifier that let's the navigateToDashboard function know which NavDirections to return.
Code for the class managing the conditional logic.
class DynamicNavImpl(private val featureFlagService: FeatureFlagService) : DynamicNav {
override fun navigateToDashboard(navDirectionsMap: Map<Int, NavDirections>): NavDirections {
val destinationIdRes = if (featureFlagService.isDashboardV2Enabled()) {
R.id.dashboardV2Fragment
} else {
R.id.dashboardFragment
}
return navDirectionsMap[destinationIdRes] ?: handleNavDirectionsException(destinationIdRes)
}
private fun handleNavDirectionsException(destinationIdRes: Int): Nothing {
throw IllegalStateException("Destination $destinationIdRes does not have an accompanying set of NavDirections. Are you sure you added NavDirections for it?")
}
}
Call site examples
navigate(
dynamicNav.navigateToDashboard(
mapOf(
Pair(R.id.dashboardFragment, PhoneVerificationFragmentDirections.phoneVerificationToDashboard()),
Pair(R.id.dashboardV2Fragment, PhoneVerificationFragmentDirections.phoneVerificationToDashboardV2())
)
)
)
navigate(
dynamicNav.navigateToDashboard(
mapOf(
Pair(R.id.dashboardFragment, EmailLoginFragmentDirections.emailLoginToDashboard()),
Pair(R.id.dashboardV2Fragment, EmailLoginFragmentDirections.emailLoginToDashboardV2())
)
)
)
Looking at the call site, you could see how this could be problematic. If I ever want to add a new potential destination, let's say dashboardV3Fragment, then I'd have to go to each call site and add another Pair.
This almost defeats the purpose of having the DynamicNavImpl class. So this is where I am stuck. I want to be able to encapsulate the various variables involved in deciding what destination to go to, but it seems with how NavDirections are implemented, I'm not able to.
I went between a few different approaches, and I landed on something that still doesn't feel ideal, but works for my use case.
I completely abandoned the idea of using a central dynamic navigation manager. Instead, I decided on having a "redirect" or "container" Fragment that decides what Fragment to show.
So here's the new code inside of the DashboardRedirectFragment
childFragmentManager.beginTransaction().replace(
R.id.dashboard_placeholder,
if (featureFlagService.isDashboardV2Enabled()) {
DashboardV2Fragment.newInstance()
} else {
DashboardFragment.newInstance()
}
).commit()
The way I'm using this is by registering a new destination in my nav graph called dashboardRedirectFragment, and anything in the graph that needs access to the dashboard use the dashboardRedirectFragment destination.
This fully encapsulates the dynamic navigation logic in the redirect Fragment, and allows me to continue using my nav graph as expected.
I added a nullable argument to my start destination:
<?xml version="1.0" encoding="utf-8"?>
<navigation xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:id="#+id/nav_graph"
app:startDestination="#id/startDest">
<fragment android:id="#+id/startDest"
android:name="com.myapp.MyStartFragment"
android:label="Start"
tools:layout="#layout/fragment_start">
<argument
android:name="dataObject"
app:argType="com.myapp.MyDataObject"
android:defaultValue="#null"
app:nullable="true"/>
...
</fragment>
...
</navigation>
But when I load my app, I get the following exception:
java.lang.IllegalStateException: Fragment MyStartFragment{a4ffd1f (ca52d4dc-ff36-4a93-8ebf-f11af7b7d5aa) id=0x7f080145} has null arguments
at com.myapp.MyStartFragment$$special$$inlined$navArgs$1.invoke(FragmentNavArgsLazy.kt:42)
at com.myapp.MyStartFragment$$special$$inlined$navArgs$1.invoke(Unknown Source:0)
at androidx.navigation.NavArgsLazy.getValue(NavArgsLazy.kt:44)
at androidx.navigation.NavArgsLazy.getValue(NavArgsLazy.kt:34)
at com.myapp.MyStartFragment.getArgs(Unknown Source:27)
at com.myapp.MyStartFragment.onAttach(MyStartFragment.kt:85)
And the exception is triggered by this piece of code in MyStartFragment:
private val args: MyStartFragmentArgs by navArgs()
override fun onAttach(context: Context) {
super.onAttach(context)
val title = if(this.args.dataObject == null) getString(R.string.start_list_title) else this.args.dataObject!!.name
...
}
And here is the code for MyDataObject:
#Parcelize
data class MyDataObject (
val id: String,
val name: String,
val externalIdentifier: String
val type: MyDataEnumType,
var responsibleUser: SomeOtherParcelableClass?
): Parcelable
What I don't understand is that my start destination doesn't get passed arguments properly by the navigation controller. Am I missing something here?
Hello I am assuming you want to achieve something like below
BeforeFragment --arg--> StartFragment --> AfterFragment
This flows are similar first time user, returning user flows. Here BeforeFragment is last fragment in login_nav_graph nested graph. StartFragment is the starting destination of main_nav_graph. StartFragment is the first screen returning user sees.
So in BeforeFragment you may set args as follows
val userJohn:User = User(34, "John", 645, UserType.TYPE2, Guardian("Mike"))
val action = BeforeFragmentDirections.actionGlobalStart(userJohn)
findNavController().navigate(action)
and in StartFragment you may do following as you already did
title = if(this.args.user == null)
getString(R.string.user_name) // mocks loading saved user name
else
this.args.user?.name // when user is first time user read from passed args
Sample Repo can be found here
My best guess
This issue is due to bug in older navigation version, so use 2.2.0-alpha01 which is I am using in the sample repo.
In order to fix errors that occurs when moved to new navigation version
in you module gradle file add following
android {
...
kotlinOptions {
jvmTarget = "1.8" // set your Java version here
}
}
This fixes the error
Cannot inline byte code ...
Keep in mind passing complex objects as arguments is not recommended. Quoting from docs.
In general, you should strongly prefer passing only the minimal amount
of data between destinations. For example, you should pass a key to
retrieve an object rather than passing the object itself, as the total
space for all saved states is limited on Android. If you need to pass
large amounts of data, consider using a ViewModel as described in
Share data between fragments.
If this fixes your issue please confirm the answer, since I spent lot of time preparing this post.
If you create a fragment with a bundle, but without NavController.navigate, and you still want to keep "by navArgs()" in destination fragment e.g because you want to use it in launchFragmentInContainer when testing, then use try/catch. The IllegalStateException is thrown before you start checking nullable type.
private fun updateArguments(){
this.title = try{
val args: DestinationFragmentArgs by navArgs()
if (args.dataObject?.name == ""){
// default value provided so check whether bundle was used
arguments?.getString("name") ?: ""
}else {
args?.venueId
}
}catch (ex: Exception){
// fragment created without using NavController.navigate
arguments?.getString("venueId") ?: ""
}
}
Room is very fast, so retrieving an object from the id as an argument should not be an issue, so try using primitive types for your arguments if possible, otherwise use Parcelable or Serialize for inexpensive objects.
I want to inject a new destination to the current nav graph.
I notice NavGraph has a method void addDestination(#NonNull NavDestination node) but I can't find a proper way to create a NavDestination and navigate to it using navController.navigate(R.id.new_dest_id).
I've got two working ways:
navController.graph.addDestination(ActivityNavigator(this).createDestination().apply {
id = R.id.new_dest
setComponentName(ComponentName(context, NewActivity::class.java))
// or setIntent
})
or this
navController.graph.addDestination(
navController.navigatorProvider.getNavigator(ActivityNavigator::class.java)
.createDestination().apply {
id = R.id.new_dest
setComponentName(ComponentName(context, NewActivity::class.java))
}
)
There is also a DSL builder ActivityNavigatorDestinationBuilder.
Fragment is similar. Change ActivityNavigator to FragmentNavigator and use different setters.
I also made a mistake when I added a destination in one graph and tried to navigate to the new destination in another graph. of course that never work.
Did you try to pass the ID of your newly created Destination in navigate(), like navigate(myDes.getId())