How to reuse a fragment in different navigation graphs with safe args enabled? - android

I'm trying to reuse a fragment in different navigation graphs with safe args enabled. I noticed that if the actions are different I get a compilation error. This is because the xxxFragmentDirections autogenerated code will only generate one of the actions.
In nav_graph_1.xml:
<navigation
...
<fragment
android:id="#+id/myFragment"
android:name="com.example.android.MyFragment">
<action
android:id="#+id/next_action"
app:destination="#+id/dest_one" />
</fragment>
...
In nav_graph_2.xml:
<navigation
...
<fragment
android:id="#+id/myFragment"
android:name="com.example.android.MyFragment">
<action
android:id="#+id/other_action"
app:destination="#+id/other_dest" />
</fragment>
...
A simple use case: A bank app that has two flows: withdraw and deposit, therefore you could have two nav graphs. You could have an AmountFragment where you could just enter a number and this could be reused to either withdraw or deposit. However, depending on the flow, the actions/destinations could be different.
Then, how would it be possible to reuse this fragment?

Use navigate() with bundle instead of actions in edge cases. Don’t call
findNavController().navigate(FragmentDirections.goToDetailFragment(id))
but instead use
findNavController().navigate(R.id.DetailFragment, bundleOf("id" to 5))
This way you don’t rely on the generated direction but can still use the Navigation and SafeArgs features of the DetailFragment.
https://code.allaboutapps.at/articles/android-jetpack-navigation-pitfalls/#reuse-fragments-in-multiple-navigation-graphs

This can be achieved by providing the same action ID in both the navigation graph.
In nav_graph_1.xml:
<navigation
...
<fragment
android:id="#+id/myFragment"
android:name="com.example.android.MyFragment">
<action
android:id="#+id/next_action"
app:destination="#+id/dest_one" />
</fragment>
...
In nav_graph_2.xml:
<navigation
...
<fragment
android:id="#+id/myFragment"
android:name="com.example.android.MyFragment">
<action
android:id="#+id/next_action"
app:destination="#+id/other_dest" />
</fragment>
...
In fragment, navigation can be performed like
NavHostFragment.findNavController(<myFragment>).navigate(R.id.next);

It might not be what you ask, but I faced the problem o re-usability of code in a fragment that I needed in different navigation graphs.
The fragment class that needs to be reused should be declared as abstract:
abstract class ReusableFragment(val type: ReusableEnum): Fragment(){
enum class ReusableEnum{Type1, Type2}
// the rest of your logic will use
// 'type' variable in when() blocks to determine
// specific logic for each case
}
class Type1Fragment: ReusableFragment(ReusableEnum.Type1)
class Type2Fragment: ReusableFragment(ReusableEnum.Type2)
This way, Type1Fragment and Type2Fragment are available in NavigationGraph as independent fragments.

Related

When I add the same fragment to second navigation graph, don´t recognize the Action Class

I need to add the same fragment to different navigation graphs. This is piece of code of first graph (mobile_navigation)
<fragment
android:id="#+id/nav_contactanos"
android:name="com.engie.mexico.micuenta.Fragments.FragmentContactanosCliente"
tools:layout="#layout/fragment_contactanos_cliente" >
<action
android:id="#+id/action_nav_contactanos_to_nav_aviso_privacidad"
app:destination="#id/nav_aviso_privacidad" />
<action
android:id="#+id/action_nav_contactanos_to_nav_terminos_condiciones"
app:destination="#id/nav_terminos_condiciones" />
<action
android:id="#+id/action_nav_contactanos_to_nav_mensaje_usuario"
app:destination="#id/nav_mensaje_usuario" />
</fragment>
The second fragment of code in second navigation graph (navigation_inicial)
<fragment
android:id="#+id/nav_contactanos_cliente"
android:name="com.engie.mexico.micuenta.Fragments.FragmentContactanosCliente"
tools:layout="#layout/fragment_contactanos_cliente" >
<action
android:id="#+id/action_nav_contactanos_cliente_to_nav_mensaje_usuario"
app:destination="#id/nav_mensaje_usuario" />
<action
android:id="#+id/action_nav_contactanos_cliente_to_nav_terminos_condiciones"
app:destination="#id/nav_terminos_condiciones" />
<action
android:id="#+id/action_nav_contactanos_cliente_to_nav_aviso_privacidad"
app:destination="#id/nav_aviso_privacidad" />
</fragment>
When I try to MakeProject, the build output shows me the error :
C:\Android\MiCuenta\app\src\main\java\com\engie\mexico\micuenta\Fragments\FragmentContactanosCliente.java:478: error: cannot find symbol
FragmentContactanosClienteDirections.ActionNavContactanosToNavMensajeUsuario action = symbol: class ActionNavContactanosToNavMensajeUsuario
location: class FragmentContactanosClienteDirections
But when I comment the second piece of code (navigation_inicial), the problem disappears.
The thing is that I need the second piece of code because I need to recall fragment and share with it bundles, with other information and show different things...
How can achieve this?
I also share the design of graphs:
mobile_navigation
mobile_navigation.xml (design)
navigation_inicial
navigation_inicial.xml (design)
The name of the Directions class is based on the name of the Fragment and there can only be one instance of a particular class at a time. As per this issue with Safe Args, there is no warning when you are overwriting one Directions class from one from another graph - the last one wins. This is why reusing the same fragment in a different graph invalidates the Directions class from the first graph.
Of course, if your second graph's fragment has different actions, different arguments, or anything at all different about it, it should have a different fragment class as well - if your single fragment class were to use arguments or actions from another graph, they would fail as they would not exist in that graph.
As mentioned in that bug:
For a destination with an android:name, you'd want to include that android:name in only one place in your graph, referencing that shared destination from all of the places that need access to it (as a destination can access any sibling destinations of their parent graph).

Pass arguments safely to nested graph with Navigation Component

I'm using Android Jetpack Navigation Component.
I have a nested nav graph with id, say R.id.nested_graph
The first Fragment of the nested graph receives one parameter.
<navigation
android:id="#+id/nested_graph"
android:label="Nested Graph"
app:startDestination="#id/firstFragment">
<fragment
android:id="#+id/firstFragment"
android:name="...."
android:label="....">
<argument
android:name="item_id"
app:argType="integer" />
</fragment>
[...]
</navigation>
How can I pass the parameter to the nested graph using safe args?
At the moment, I need to pass the argument manually in the bundle, using the API that receives the id of the nested graph directly:
val args = Bundle()
args.putInt("item_id", itemId)
navController.navigate(R.id.nested_graph, args)
I'd like to use safe args, and do something like:
val directions = OrigininFragmentDirections.nestedGraph(itemId)
navController.navigate(directions)
But when trying that, I get the following error at build time:
Too many arguments for public final fun nestedGraph(): NavDirections defined
The issue is that the nav graph preprocessing is generating the factory method to create the NavDirections object without the required parameter in the signature.
The declaration of the nested graph looks like this:
After some trial and error experimentation (I don't think it's officially documented by Google, or at least I could not find it), I've discovered that navigating to nested nav graphs passing arguments safely can be done:
You need to add the argument XML object the first fragment expects in the root of the nested fragment itself.
In my case, the fragment with id firstFragment, which is the first fragment in the nested graph receives:
<argument
android:name="item_id"
app:argType="integer" />
Hence, I need to add that argument to the nested graph:
<navigation
android:id="#+id/nested_graph"
android:label="Nested Graph"
app:startDestination="#id/firstFragment">
<argument
android:name="item_id"
app:argType="integer" />
<fragment
android:id="#+id/firstFragment"
android:name="...."
android:label="....">
<argument
android:name="item_id"
app:argType="integer" />
</fragment>
[...]
</navigation>
Now I can navigate to it with:
val directions = OrigininFragmentDirections.nestedGraph(itemId)
navController.navigate(directions)
Note that the navigation graph editor does not do it for you. This needs to be done manually in the XML code.
#GaRRaPeTa answer is almost correct, but if you navigate from main graph to nested graph by action using SafeArgs, you must also add an argument to the action:
<navigation xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:id="#+id/graph_main"
app:startDestination="#id/mainFragment">
<fragment
android:id="#+id/mainFragment"
android:name="com.example.MainFragment">
<action
android:id="#+id/toNestedGraph"
app:destination="#id/graph_nested">
<argument
android:name="arg_name"
app:argType="string" />
</action>
</fragment>
</navigation>

Android : Difference between action id and fragment id in navigation component

I have a question that is bugging me for a long time,
In navigation component, when using .navigate(int resId) what makes a difference in passing an action id vs fragment id?
example:
<fragment android:id="#+id/loginFragment"
android:name="com.example.myapp.ui.main.LoginFragment"
android:label="#string/login"
tools:layout="#layout/fragment_login" >
<action
android:id="#+id/action_login_to_emailLoginFragment"
app:destination="#id/emailLoginFragment"
app:popEnterAnim="#anim/slide_in_right"
app:popExitAnim="#anim/slide_out_right"
app:popUpTo="#+id/emailLoginFragment"
app:popUpToInclusive="true"/>
</fragment>
<fragment android:id="#+id/emailLoginFragment"
android:name="com.example.myapp.ui.main.EmailLoginFragment"
android:label="EmailLoginFragment"
tools:layout="#layout/fragment_login_email" />
In the above scenario, what will be the difference if
1) use .navigate(R.id.action_login_to_emailLoginFragment);
2) use .navigate(R.id.emailLoginFragment);
And i have one more query, I know that .navigate(int resId) will replace the fragment, in that case how to retain state of views in previous fragment?
All of the other attributes on an action - i.e., the popEnterAnim, popExitAnim, popUpTo, and popUpToInclusive are part of a NavOptions object that is automatically applied when you use that action id.
Therefore when you use navigate(R.id.emailLoginFragment), none of the other fields are applied: you won't pop anything off the back stack nor will any animations be applied. To duplicate what the action provides, you'd need to use the navigate(int, Bundle, NavOptions) method, manually constructing the correct NavOptions.

Passing an argument between two fragments each of them in a different Navigation Graph

Using Android Navigation Component. I have two Navigation Graphs (each of them with their own flow), lets say navigation graphs A and B. I navigate successfully from A to B, but I can't manage to pass an argument from the last Fragment included in graph A to the start Fragment that belongs to graph B.
I am able to pass arguments between fragments that belong to the same graph, but the function to set the arguments is not been generated when navigating between to navigation graphs.
I am trying to accomplish that using safeargs.
Here is the navigation graphs code:
Navigation Graph A:
<navigation android:id="#+id/nav_graph_a"
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
app:startDestination="#id/fragment1">
<fragment
android:id="#+id/fragment1"
android:name="com.mypackage.fragments.Fragment1"
android:label="Fragment1">
<action
android:id="#+id/action_fragment1_to_fragment2"
app:destination="#id/fragment2"/>
</fragment>
<fragment
android:id="#+id/fragment2"
android:name="com.mypackage.fragments.Fragment2"
android:label="Fragment2">
<argument
android:name="thisArgumentAlwaysArrive"
android:defaultValue="null"
app:argType="string"/>
<action
android:id="#+id/action_fragment2_to_nav_graph_b"
app:destination="#id/nav_graph_b"/>
</fragment>
<include app:graph="#navigation/nav_graph_b"/>
Navigation Graph B:
<navigation android:id="#+id/nav_graph_b"
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
app:startDestination="#id/fragment3">
<fragment
android:id="#+id/fragment3"
android:name="com.mypackage.fragments.Fragment3"
android:label="Fragment3">
<argument
android:name="thisArgumentNeverArrive"
app:argType="string"/>
</fragment>
Any idea on how to accomplish this?
As stated in the documentation:
Note: Safe Args do not support cross-module navigation, as there is no direct action to the destination. In the previous example, although a Directions class would be generated for the target destination in settings, you aren't able to access the generated class from the classpath of the list module.
But you can use a bundle instead of nav_args like this:
From
FristFragmentDirections.actionFirstFragmentToSecondFragment(your_parameter).let { action ->
findNavController().navigate(action)
}
To
FristFragmentDirections.actionFirstFragmentToSecondFragment().let { action ->
findNavController().navigate(
action.actionId,
Bundle().apply { putString("parameter_key", your_parameter) }
)
}
Safe Args is the plugin for Gradle that creates the classes ClassArgs for the destinations.
The default controller provides a way to navigate to another NavGraph, in this case there is an implementation of a Navigator called NavGraphNavigator.
This navigator provides you with a navigate(NavGraph destination,Bundle args) function that passes the arguments to the start fragment.
So, the best way I have found to do what you are trying to achieve is to use the generated class to generate a bundle with the needed arguments.
var myBundle = YourFragmentIdInNavGraphArgs.Builder(var args..).toBundle()
and then use the default NavController to navigate to a navGraph destination like this.
view.findNavController().navigate(R.id.your_action_to_nav_graph_id,myBundle)
the implementation of the Navigator will take care of passing the arguments to the start fragment on the destination.
Hope it helps to some one.

Using DialogFragment as one of the Fragment in Android NavController

I have a graph defined using XML and I added a DialogFragment as one of the Fragment in my NavGraph. when I call NavController.navigate with resId and bundle on that nav controller I don't see any Dialog being displayed. Is there any way I can use DialogFragment instead of standard Fragment?
<fragment
android:id="#+id/noLoginDialog"
android:name="com.ram.view.NoLoginDialog"
android:label="NoLoginDialog">
<argument
android:name="argTitle"
android:defaultValue="null"
app:type="string"/>
<argument
android:name="argBody"
android:defaultValue="null"
app:type="string"/>
<argument
android:name="argButton"
android:defaultValue="null"
app:type="string"/>
</fragment>
and my action is defined as below
<container_fragment
android:id="#+id/homeFragment"
android:name="com.ram.home.HomeFragment"
android:label="HomeFragment">
<action
android:id="#+id/action_homeFragment_to_noAuthAlertDialog"
app:destination="#id/noLoginDialog"/>
</container_fragment>
My other actions with activity and fragments works just fine.
hum, Navigation Library will never open an Fragment as Dialog, it just replace the first (home destination) to other destination in your NavHostFragment.
Google said:
A destination is any place you can navigate to in your app. While
destinations are usually Fragments representing specific screens..
Please provide more information, like your navigation code (java/kotlin).
Take a read on this official Google post to understand more about Navigation:
https://developer.android.com/topic/libraries/architecture/navigation/navigation-implementing
Yes you can call up DialogFragment from navigation, you will just have to explicitly tell the navigation that you are going to use dialog
<dialog
android:id="#+id/simCancelFinish"
android:name="mk.telekom.kiosk.ui.dialogs.SimCancelFinish"
android:label="SimCancelFinish" >
<action
android:id="#+id/action_simCancelFinish_to_stornoSimFragment"
app:destination="#id/stornoSimFragment"
app:popUpTo="#id/stornoSimFragment"
app:popUpToInclusive="true" />
</dialog>

Categories

Resources