Unable to navigate to a destination in other graph - android

Using Android Navigation component in our code with SAMF (Single Activity, Multiple Fragments) approach for each functionality. Say each functionality will have own navigation graph intended to navigate between them.
e.g., the graphs are graph_a, graph_b, graph_c with its own fragments and designed to navigate as below:
graph_a >> graph_b >> graph_c
and it works as expected.
Now, there are scenarios where a Fragment(frag_b) might need to navigate another one in different navigation graph like
frag_b(graph_b) >> frag_c(graph_a)
I found the optimal solution, in this case, could be navigate using URI and tried below.
val deepLink = Uri.parse("app://login/user-signup")
findNavController().navigate(deepLink)
and added the following deeplink for to be called fragment in the respective graph.
<navigation xmlns:android="http://schemas.android.com/apk/res/android"
android:id="#+id/graph_login_flow"
app:startDestination="#id/userSignInFragment">
...
<fragment
android:id="#+id/userSignUpFragment"
android:name="com.app.login.ui.UserSignUpFragment"
android:label="EmailSignUpFragment"
tools:layout="#layout/fragment_user_sign_up">
<action
android:id="#+id/action_userSiUpFrag_to_emailVeriFrag"
app:destination="#id/emailVerificationFragment" />
<deepLink
android:id="#+id/deepLink_to_userSignUpFragment"
app:uri="app://login/user-signup" />
</fragment>
<include app:graph="#navigation/graph_home_flow" />
...
</navigation>
As per Android documentation, can navigate to any destination regardless of graphs but my attempt results in the following error.
Is there any better approach?
java.lang.IllegalArgumentException: navigation destination with deepLink app://login/user-signup is unknown to this NavController
at androidx.navigation.NavController.navigate(NavController.java:916)
at androidx.navigation.NavController.navigate(NavController.java:894)
at androidx.navigation.NavController.navigate(NavController.java:880)
at com.app.home.profile.ProfileFragment.onViewCreated(ProfileFragment.kt:82)
at androidx.fragment.app.FragmentStateManager.createView(FragmentStateManager.java:298)
at androidx.fragment.app.FragmentManager.moveToState(FragmentManager.java:1232)
at androidx.fragment.app.FragmentManager.addAddedFragments(FragmentManager.java:2390)
at androidx.fragment.app.FragmentManager.executeOpsTogether(FragmentManager.java:2125)
at androidx.fragment.app.FragmentManager.removeRedundantOperationsAndExecute(FragmentManager.java:2081)
at androidx.fragment.app.FragmentManager.execPendingActions(FragmentManager.java:1977)

Related

Navigate to different fragments in another nav graph

I have to navigate from fragment in module 1 and navgraph #1 (let's call it first_nav_graph) to different fragments in another module and another navgraph #2 (let's call it second_nav_graph).
When I navigate to startDestination of second_nav_graph it works correctly - I just include second_nav_graph to first_nav_graph and create action:
<include app:graph="#navigation/second_nav_graph" />
...
<dialog
android:id="#+id/card_menu_fragment"
android:name="ru.bank.mobile.presentation.ui.product_menus.CardMenuFragment"
android:label="CardMenuFragment"
tools:layout="#layout/fragment_menu">
<action
android:id="#+id/actionCardMenuFragmentToSecondNavGraph"
app:destination="#id/second_nav_graph"/>
So we can easily navigate to startDestination of second_nav_graph. The problem is that I need to navigate to two different fragments of second_nav_graph. I added such actions to first_nav_graph:
<include app:graph="#navigation/second_nav_graph" />
...
<dialog
android:id="#+id/card_menu_fragment"
android:name="ru.bank.mobile.presentation.ui.product_menus.CardMenuFragment"
android:label="CardMenuFragment"
tools:layout="#layout/fragment_menu">
<action // navigates correctly
android:id="#+id/actionCardMenuFragmentToSecondNavGraph"
app:destination="#id/second_nav_graph"/>
<action // app crashes
android:id="#+id/actionCardMenuFragmentToIncreaseLimitFragment"
app:destination="#id/increaseLimitFragment" />
<action // app crashes
android:id="#+id/actionCardMenuFragmentToIncreaseLimitStatusFragment"
app:destination="#id/increaseLimitStatusFragment" />
But when I push button that navigates to specific fragment in second_nav_graph (with findNavController().navigate()) my app crashes with exception:
E/AndroidRuntime: FATAL EXCEPTION: main
Process: ru.otpbank.mobile, PID: 11072
java.lang.IllegalArgumentException: Navigation destination ru.otpbank.mobile:id/increaseLimitFragment referenced from action ru.otpbank.mobile:id/actionCardMenuFragmentToIncreaseLimitFragment cannot be found from the current destination Destination(ru.otpbank.mobile:id/card_menu_fragment) label=CardMenuFragment
second_nav_graph.xml looks like this:
<?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/second_nav_graph"
app:startDestination="#id/increaseLimitFragment">
<fragment
android:id="#+id/increaseLimitFragment"
android:name="com.example.credit_limit_increase.limit_increase_application.IncreaseLimitFragment"
android:label="IncreaseLimitFragment" />
<fragment
android:id="#+id/increaseLimitStatusFragment"
android:name="com.example.credit_limit_increase.limit_increase_status.IncreaseLimitStatusFragment"
android:label="fragment_increase_limit_status"
/>
</navigation>
Could you please help me to understand the reason of this behavior and any ways to fix it?
I found in documentation that the only way to access another graph is through its startDestination:
The nested graph encapsulates its destinations. As with a root graph,
a nested graph must have a destination identified as the start
destination. Destinations outside of the nested graph, such as those
on the root graph, access the nested graph only through its start
destination.
So it seems like there are no ways to navigate to different fragments of a nested graph from another graph.

Setting a graph programatically using setGraph() results in app's inability to restore the stack after process death (Jetpack Navigation)

I am currently in a situation where it might be unfeasible to refactor the whole app in order to make a more cohesive whole but the current approach to using Jetpack Navigation is causing crashes.
The app has multiple graphs, for the sake of simplicity let's call them:
Graph A,
Graph B,
Graph C
Graph A is set in the activity layout through app:navGraph and if it remains the only used graph the application can and will restore it's state after process death.
However at certain moments the app programatically switches the graphs by using the navController of the FragmentContainerView and calling setGraph(graphResId: Int) with the new graphs id provided. Before that manually inflating the graph was used as well.
Both approaches of setting the graph result in a crash after the first restart after process death and an exception thrown that says:
Unable to start activity ComponentInfo{com.appname.dev/com.appname.activity.MainActivity}: java.lang.IllegalStateException: Restoring the Navigation back stack failed: destination com.appname.dev:id/graph_b cannot be found from the current destination null
The exception occurs in MainActivity onCreate's call to super.onCreate()
The only solution in my mind that I can come up with is to include the other graphs in the first graph, use actions to navigate to the other graphs and therefor avoid switching graphs. However due to the way the application has been made that might end up being quite time consuming.
I am wondering if I am missing an important step that could mediate this crash.
Alright, I found a solution and it doesn't need any trivial changes to the code either.
nav_a.xml (or Graph A in your case) should look like this:
<?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_a"
app:startDestination="#id/mainFragment">
<action android:id="#+id/action_to_nav_c"
app:destination="#id/nav_c"/>
<action android:id="#+id/action_to_nav_b"
app:destination="#id/nav_b"/>
<fragment
android:id="#+id/mainFragment"
android:name="com.example.fragment.MainFragment"
android:label="#string/home" />
<include app:graph="#navigation/nav_c" />
<include app:graph="#navigation/nav_b" />
</navigation>
Notice, the actions action_to_nav_c and action_to_nav_b are NOT inside of any other tag. This way you can navigate them from anywhere without getting any errors.
To navigate to nav_c (Graph C in your case), you can do this inside the host activity in Kotlin with safe args:
navController.navigate(NavADirections.actionToNavC())
Or directly (in Java)
navController.navigate(R.id.action_to_nav_c);
Lastly, your nav_c.xml may look like this:
<?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_c"
app:startDestination="#id/settingsFragment">
<fragment
android:id="#+id/settingsFragment"
android:name="com.example.fragment.SettingsFragment"
android:label="#string/settings" />
</navigation>

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).

Android fragment navigation: how to navigate to an arbitrary fragment programmatically

I am learning to use Android fragment navigation.
Now I can follow the sample to use ID to navigate to a specific target fragment defined in the navigation graph. Like following:
<fragment android:id="#+id/a"
android:name="com.example.myapplication.FragmentA"
android:label="a"
tools:layout="#layout/a">
<action android:id="#+id/action_a_to_b"
app:destination="#id/b"
app:enterAnim="#anim/nav_default_enter_anim"
app:exitAnim="#anim/nav_default_exit_anim"
app:popEnterAnim="#anim/nav_default_pop_enter_anim"
app:popExitAnim="#anim/nav_default_pop_exit_anim"/>
</fragment>
viewTransactionsButton.setOnClickListener { view ->
view.findNavController().navigate(R.id.viewTransactionsAction)
}
However, I have a special use case that I want to programmatically navigate to a destination fragment, which is not defined in the navi graph. How can I achieve it like following:
itemView.setOnclickListener { view ->
val targetFragmentClass = getTargetFragmentClass(view)
//How can I findNavController().navigate() to created targetFragmentClass instance?
}
I checked the document of navigation controller, all versions of its navigate() method need some resource ID which means I have to define the action or destination in the XML. But I want something like the legacy fragment manger APIs, which I can instantiate any fragment class and create a transaction to apply it.
So, my questions is:
Can I use navigation component to navigate to any arbitrary fragment purely programmatically?
If I have to use fragment manager/transaction to deal with this case, can I make it work with the navigation controller together?
Thanks a lot.

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.

Categories

Resources