I want to migrate my project to use navigation components.
In my Activity there is a bottom navigation, which navigates through different instances of the same Fragment (with different arguments).
Here explained about bottomNavigation support.
But is it possible to reuse same Fragment in the same navigation graph, with different IDs and params?
I can't find way in google documentations.
You should be able to define your nav_graph with multiple destinations and reuse the same fragment. Something like this,
<navigation xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:id="#+id/mobile_navigation"
app:startDestination="#+id/navigation_tab1">
<fragment
android:id="#+id/navigation_tab1"
android:name="com.myapp.MyFragment"
android:label="Tab1">
<action
android:id="#+id/action_goto_page1"
app:destination="#id/navigation_page1" />
<argument
android:name="tab1_arg1"
android:defaultValue="Default"
app:argType="string" />
</fragment>
<fragment
android:id="#+id/navigation_tab2"
android:name="com.myapp.MyFragment"
android:label="Tab2">
<action
android:id="#+id/action_goto_page2"
app:destination="#id/navigation_page2" />
<argument
android:name="tab2_arg1"
android:defaultValue="Default"
app:argType="string" />
</fragment>
<fragment
android:id="#+id/navigation_tab3"
android:name="com.myapp.MyFragment"
android:label="Tab3">
<action
android:id="#+id/action_goto_page3"
app:destination="#id/navigation_page3" />
<argument
android:name="tab3_arg1"
android:defaultValue="Default"
app:argType="string" />
</fragment>
</navigation>
However, it's best to refactor your code to have multiple Fragments (each doing one thing) for better maintenance and clean code.
Related
Given I have a Jetpack Navigation Graph as below, where by
BlankFragment1 -> BlankFragment2 -> BlankFragment3 -> BlankFragment4 -> BlankFragment5
<?xml version="1.0" encoding="utf-8"?>
<navigation xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
xmlns:android="http://schemas.android.com/apk/res/android"
app:startDestination="#id/blankFragment1">
<fragment
android:id="#+id/blankFragment1"
android:name="com.example.cashdog.cashdog.BlankFragment1"
android:label="#string/label_blank1"
tools:layout="#layout/fragment_blank1" >
<action
android:id="#+id/action_blankFragment1_to_blankFragment2"
app:destination="#id/blankFragment2" />
</fragment>
<fragment
android:id="#+id/blankFragment2"
android:name="com.example.cashdog.cashdog.BlankFragment2"
android:label="#string/label_blank2"
tools:layout="#layout/fragment_blank2" >
<action
android:id="#+id/action_blankFragment2_to_blankFragment3"
app:destination="#id/blankFragment3" />
</fragment>
<fragment
android:id="#+id/blankFragment3"
android:name="com.example.cashdog.cashdog.BlankFragment3"
android:label="#string/label_blank3"
tools:layout="#layout/fragment_blank3" >
<action
android:id="#+id/action_blankFragment3_to_blankFragment4"
app:destination="#id/blankFragment4" />
</fragment>
<fragment
android:id="#+id/blankFragment4"
android:name="com.example.cashdog.cashdog.BlankFragment4"
android:label="#string/label_blank4"
tools:layout="#layout/fragment_blank4" >
<action
android:id="#+id/action_blankFragment4_to_blankFragment5"
app:destination="#id/blankFragment5" />
</fragment>
<fragment
android:id="#+id/blankFragment5"
android:name="com.example.cashdog.cashdog.BlankFragment5"
android:label="#string/label_blank5"
tools:layout="#layout/fragment_blank5" />
</navigation>
If I have another Fragment, named FragmentSomething, that can be open by and of the FragmentBlank1 to FragentBlank5
Is there a way to update the NavGraph above for my FragmentSomething, without the need to add actions for all 5 of them?
Apparently, there's something called the global action
https://developer.android.com/guide/navigation/navigation-global-action
We just need to add this
<action android:id="#+id/action_global_somethingFragment"
app:destination="#id/somethingFragment"/>
<fragment
android:id="#+id/somethingFragment"
android:name="com.example.cashdog.cashdog.SomethingFragment"
android:label="#string/label_something"
tools:layout="#layout/fragment_something" />
Or we can navigate the code directly to somethingFragment using
findNavController().navigate(R.id.somethingFragment)
I have the problem that my sample app crashes when I deeplink from one graph part to the other one.
My structure is simple:
Two tabs with own graphs which have each two possible fragments
When I navigate to a detail page (in my sample a book) this just works from my "Shelf" but not from the home screen.
Here is my full graph:
<?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"
xmlns:tools="http://schemas.android.com/tools"
android:id="#+id/sample"
app:startDestination="#id/tab_home">
<navigation
android:id="#+id/tab_home"
app:startDestination="#id/frag_home">
<fragment
android:id="#+id/frag_home"
android:name="eu.rekisoft.android.navbug.HomeFragment"
tools:layout="#layout/fragment_home">
<action
android:id="#+id/cause_bug"
app:destination="#id/frag_hint"/>
<action
android:id="#+id/causeCrash"
app:destination="#id/frag_book">
<argument
android:name="name"
app:argType="string"/>
</action>
</fragment>
<fragment
android:id="#+id/frag_hint"
android:name="eu.rekisoft.android.navbug.HintFragment"
android:label="Hint"
tools:layout="#layout/fragment_hint"/>
</navigation>
<navigation
android:id="#+id/tab_books"
app:startDestination="#id/frag_shelf">
<fragment
android:id="#+id/frag_shelf"
android:name="eu.rekisoft.android.navbug.ShelfFragment"
android:label="Shelf Fragment"
tools:layout="#layout/fragment_shelf">
<action
android:id="#+id/open_book"
app:destination="#id/frag_book">
<argument
android:name="name"
app:argType="string"/>
</action>
</fragment>
<fragment
android:id="#+id/frag_book"
android:name="eu.rekisoft.android.navbug.BookFragment"
android:label="Book Fragment"
tools:layout="#layout/fragment_book">
<argument
android:name="name"
app:argType="string"/>
</fragment>
</navigation>
</navigation>
Here is my crash:
java.lang.IllegalArgumentException: Navigation destination eu.rekisoft.android.navbug:id/frag_book referenced from action eu.rekisoft.android.navbug:id/causeCrash cannot be found from the current destination Destination(eu.rekisoft.android.navbug:id/frag_home) class=eu.rekisoft.android.navbug.HomeFragment
at androidx.navigation.NavController.navigate(NavController.kt:1527)
at androidx.navigation.NavController.navigate(NavController.kt:1464)
at androidx.navigation.NavController.navigate(NavController.kt:1922)
at eu.rekisoft.android.navbug.HomeFragment.onViewCreated$lambda-2$lambda-1(HomeFragment.kt:23)
at eu.rekisoft.android.navbug.HomeFragment.$r8$lambda$q53N5QXeJ-lbbpd7MjwkcKbt9_4(Unknown Source:0)
at eu.rekisoft.android.navbug.HomeFragment$$ExternalSyntheticLambda1.onClick(Unknown Source:2)
Very clear error message, but I don't know how to fix this.
If you want to checkout my code check this GitHub repo.
I've met a problem with the back stack while starting the app from a deep link (via notification).
The general idea is that I have 3 fragments (A, B, C). The A screen works as splash screen, so I've put a popUpTo and popUpToInclusive to the action, so I can never see back to it again. My deep link destination is fragment C. To achieve the proper back stack I've merged B and C into nested navigation. Unfortunately, when I press Back button in fragment B the app is showing fragment A. My guess this is because the action from A to B with popUpTo was never called when the app was started from the deep link itself. My question is: how to avoid going back from B to A in this particular scenario?
Thanks in advance! I attach the sample code:
<navigation xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:id="#+id/nav_graph"
app:startDestination="#id/splashFragment">
<fragment
android:id="#+id/A"
android:name="A"
android:label="A" >
<action
android:id="#+id/action_A_to_B"
app:destination="#id/B"
app:popUpTo="#id/A"
app:popUpToInclusive="true" />
</fragment>
<navigation android:id="#+id/nested_nav"
app:startDestination="#id/B">
<fragment
android:id="#+id/B"
android:name="B"
android:label="B">
<action
android:id="#+id/action_B_to_C"
app:destination="#id/C" />
</fragment>
<fragment
android:id="#+id/C"
android:name="C"
android:label="C">
</fragment>
</navigation>
</navigation>
And my DeepLinkBuilder (nothing special, same as the docs)
NavDeepLinkBuilder(requireContext())
.setGraph(R.navigation.nav_graph)
.setDestination(R.id.userProfileFragment)
.createPendingIntent()
.send()
You have set the navGraph that contain the splash screen as the default startDestination which lead to show it when you press back button from the nested navGraph , I think the easiest solution is to do the following :
First edit you nav file to have only one navigation like :
<navigation xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:id="#+id/nav_graph"
app:startDestination="#id/splashFragment">
<fragment
android:id="#+id/A"
android:name="A"
android:label="A" >
<action
android:id="#+id/action_A_to_B"
app:destination="#id/B"
app:popUpTo="#id/A"
app:popUpToInclusive="true" />
</fragment>
<fragment
android:id="#+id/B"
android:name="B"
android:label="B">
<action
android:id="#+id/action_B_to_C"
app:destination="#id/C" />
</fragment>
<fragment
android:id="#+id/C"
android:name="C"
android:label="C">
</fragment>
</navigation>
Secondly set a listener to navController to listen when a destination change occur , like this :
yourNavController.setOnDestinationChangedListener(this);
override onDestinationChangedListener() method and check if the fragment which about to get showed is A , then finish the app like :
#override
public void onDestinationChanged(NavController controller, NavDestination destination, Bundle arguments){
if(destination.getId()==R.id.splashFragmentId){
finish();
}
}
I had the main navigation of my app set up nicely using SafeArgs. This was my nav_graph.xml:
<navigation
android:id="#+id/nav_main"
app:startDestination="#+id/fragment1">
<fragment
android:id="#+id/fragment1"
android:name="org.example.Fragment1"
android:label="Fragment 1">
<action
android:id="#+id/fragment1_to_fragment2"
app:destination="#id/fragment2" />
</fragment>
<fragment
android:id="#+id/fragment2"
android:name="org.example.Fragment2"
android:label="Fragment 2">
<argument
android:name="ARG_CATEGORY_ID"
app:argType="long"/>
<action
android:id="#+id/fragment2_to_fragment3"
app:destination="fragment3" />
</fragment>
<fragment
android:id="#+id/fragment3"
android:name="org.example.Fragment3"
android:label="Fragment 3">
<argument
android:name="ARG_ITEM_ID"
app:argType="long"/>
</fragment>
</navigation>
And, using SafeArgs, I could happily navigate from Fragment2 to Fragment3 using:
NavDirections navDirections = Fragment2Directions.fragment2ToFragment3(myItemId);
navController.navigate(navDirections);
However, I then needed to add deep linking capability from notifications (having a backstack with arguments, as explained here), so I added a sub <navigation /> element to the end of nav_graph.xml - which became:
<navigation
android:id="#+id/nav_main"
app:startDestination="#+id/fragment1">
<fra8gment
android:id="#+id/fragment1"
android:name="org.example.Fragment1"
android:label="Fragment 1">
<action
android:id="#+id/fragment1_to_fragment2"
app:destination="#id/fragment2" />
</fragment>
<fragment
android:id="#+id/fragment2"
android:name="org.example.Fragment2"
android:label="Fragment 2">
<argument
android:name="ARG_CATEGORY_ID"
app:argType="long"/>
<action
android:id="#+id/fragment2_to_fragment3"
app:destination="fragment3" />
</fragment>
<fragment
android:id="#+id/fragment3"
android:name="org.example.Fragment3"
android:label="Fragment 3">
<argument
android:name="ARG_ITEM_ID"
app:argType="long"/>
</fragment>
<!-- This is the new sub navigation block that provides a backstack-with-arguments. -->
<navigation
android:id="#+id/nav_notification"
app:startDestination="#id/fragment2Notif">
<fragment
android:id="#+id/fragment2Notif"
android:name="org.example.Fragment2"
android:label="Fragment 2">
<action
android:id="#+id/fragment2Notif_to_fragment3Notif"
app:destination="#id/itemsGraph" />
</fragment>
<navigation
android:id="#+id/itemsGraph"
app:startDestination="#id/fragment3Notif">
<argument
android:name="ARG_ITEM_ID"
app:argType="long"/>
<fragment
android:id="#+id/fragment3Notif"
android:name="org.example.Fragment3"
android:label="Fragment 3">
<argument
android:name="ARG_ITEM_ID"
app:argType="long"/>
</fragment>
</navigation>
</navigation>
</navigation>
Notice that the file now contains two occurrences of org.example.Fragment2.
This has brought me a new problem in that the second occurrence of org.example.Fragment2 (ie, fragment2Notif) has resulted in the original Fragment2Directions.fragment2ToFragment3(...) method being no longer available.
It's been replaced with Fragment2Directions.fragment2NotifToFragment3Notif(...) which works fine when in the notification part of my app but, when called from the main part of my app, as expected, results in:
IllegalArgumentException: navigation destination
org.example:id/fragment2Notif_to_fragment3Notif is unknown to this
NavController
It seems SafeArgs has overwritten the first Fragment2Directions object, meaning the only method it has is fragment2NotifToFragment3Notif(...).
So, how do I get the old fragment2ToFragment3(...) method back?
I expect I could solve the problem by subclassing Fragment2 to effectively duplicate it with a different name, Fragment2Notif, then using Fragment2 in my main nav and Fragment2Notif in the sub nav, but is there a more elegant/preferred way?
Looking through your nav_graph.xml file, I think the idea for nested graphs(series of destinations grouped within the parent navigation graph, in this case with id nav_main) is to add deep linking capability from notifications (having a backstack with arguments).
Other than this, with the assumption that the fragment2ToFragment3(...) is overwritten to fragment2NotifToFragment3Notif(...) by SafeArgs plugin, there is no significant change as one can still navigate to Fragment3 using fragment2NotifToFragment3Notif(...) since the two nested navigation graphs combined have same structural content as the parent navigation graph except for id attribute of the <fragment> tag.
So, in essence using fragment2NotifToFragment3Notif(...) instead of fragment2ToFragment3(...) won't hurt for this case.
I have implemented a nested graph in Navigation graph, which is having 2 graphs. In the first graph, there are 3 fragments and in the second graph, there are 2 fragments. Graph 2 is included in graph 1. I want to navigate to (graph 1 step 1) to (graph 2 step 2). We can not define action between two nested fragments. So is there any way by which we can assign the dynamic destination to navigation?
Graph 1
<?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"
xmlns:tools="http://schemas.android.com/tools"
android:id="#+id/mobile_navigation"
app:startDestination="#id/dashboardFragment">
<fragment
android:id="#+id/dashboardFragment"
android:name="com.navigationgraphexample.fragments.DashboardFragment"
android:label="fragment_dashboard"
tools:layout="#layout/fragment_dashboard" >
<action
android:id="#+id/action_dashboardFragment_to_stepOneFragment2"
app:destination="#id/stepOneFragment" />
<action
android:id="#+id/action_dashboardFragment_to_sub_nav"
app:destination="#id/sub_nav" />
</fragment>
<fragment
android:id="#+id/stepOneFragment"
android:name="com.navigationgraphexample.fragments.StepOneFragment"
android:label="fragment_step_one"
tools:layout="#layout/fragment_step_one">
<action
android:id="#+id/action_stepOneFragment_to_stepTwoFragment"
app:destination="#id/stepTwoFragment" />
</fragment>
<fragment
android:id="#+id/stepTwoFragment"
android:name="com.navigationgraphexample.fragments.StepTwoFragment"
android:label="fragment_step_two"
tools:layout="#layout/fragment_step_two" />
<fragment
android:id="#+id/notificationFragment"
android:name="com.navigationgraphexample.fragments.NotificationFragment"
android:label="fragment_notification"
tools:layout="#layout/fragment_notification" >
<deepLink
android:id="#+id/deepLink"
app:uri="myapp.com/{myargs}"
/>
<argument android:name="myargs"
app:argType="integer"
android:defaultValue="1" />
</fragment>
<include app:graph="#navigation/sub_nav" />
</navigation>
Graph 2
<?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"
xmlns:tools="http://schemas.android.com/tools"
android:id="#+id/sub_nav"
app:startDestination="#id/createNewTaskFragment">
<fragment
android:id="#+id/listFragment"
android:name="com.navigationgraphexample.fragments.ListFragment"
android:label="fragment_list"
tools:layout="#layout/fragment_list" />
<fragment
android:id="#+id/createNewTaskFragment"
android:name="com.navigationgraphexample.fragments.CreateNewTaskFragment"
android:label="fragment_create_new_task"
tools:layout="#layout/fragment_create_new_task" >
<action
android:id="#+id/action_createNewTaskFragment_to_listFragment"
app:destination="#id/listFragment" />
</fragment>
</navigation>
I have checked this solution but it is not for the nested graph!
As far as class NavGraph extends NavDestination, you can try the following to change nested graph's startDestination before calling navigate to sub_nav:
val graph = navController.graph.findNode(R.id.sub_nav)
if (graph is NavGraph) {
graph.startDestination = R.id.createNewTaskFragment
}
UPD: Since version 2.4.0 all Navigation artifacts have been rewritten in Kotlin, so the setter should be accessed by it's original name:
graph.setStartDestination(R.id.createNewTaskFragment)