Navigate to different fragments in another nav graph - android

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.

Related

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

How to pop through fragment stack in NavController without loosing "parent" state

I open some main fragment FragA, which then can open other fragments, that are added to the stack, but when I press back I show a DialogC, which should clear the stack and get me back to FragA, without loosing it's state, restore it from stack, rather then creating it - to recreate it I'll have to pass some arg through the whole stack.
I tried some configs with popUpTo in different places, and also used findNacController.popUpTo(with/out_aruments) or findNavController.navigate(R.id.action_dialog_c_to_frag_a) without destination defined in action, but pop can't find action in stack, navigate wants to recrete fragment when destination is defined, withou it cannot find pop action in stack (I/NavController: Ignoring popBackStack to destination frag_a as it was not found on the current back stack)
This is sample of my nav_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">
<fragment
android:id="#+id/asdf"
android:name="SomeNaviFragment">
<action
android:id="#+id/action_asdf_to_frag_a"
app:destination="#id/frag_A" />
</fragment>
<fragment
android:id="#+id/frag_A"
android:name="FragA">
<argument
android:name="some_id"
app:argType="integer" />
<action
android:id="#+id/action_frag_a_to_frag_b"
app:destination="#id/frag_B" />
</fragment>
<fragment
android:id="#+id/frag_B"
android:name="FragB">
<action
android:id="#+id/action_frag_b_to_dialog_frag_c"
app:destination="#id/DialogFragC"/>
</fragment>
<dialog
android:id="#+id/DialogFragC"
android:name="DialogC">
<action
android:id="#+id/action_dialog_c_to_frag_a"
app:popUpTo="#id/frag_a"
app:popUpToInclusive="true"/>
</dialog>
</navigation>
In short - I wan't to go deeper from FragA through some fragments, but when the DialogC shows up in some point, I want to get back to FragA, to it's initial state. Is it possible to achive it without passing the creation arguments for FragA?
Some solution already tried, like: Navigate Back with Navigation Component with it's linked resources, but this didn't help at all.
I've used the article in your post to make this https://github.com/yoobi/backNavigation I hope it helps.
EDIT: you're looking for a combination of this and the article then.
The popUpTo attribute of an action "pops up" the back stack to a given destination before navigating. (Destinations are removed from the back stack.)
If the popUpToInclusive attribute is false or is not set, popUpTo removes destinations up to the specified destination, but leaves the specified destination in the back stack.
If popUpToInclusive is set to true, the popUpTo attribute removes all destinations up to and including the given destination from the back stack.
If popUpToInclusive is true and popUpTo is set to the app's starting location, the action removes all app destinations from the back stack. The Back button takes the user all the way out of the app.
You can also check the count of your backstack with : parentFragmentManager.backStackEntryCount

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

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.

Unable to navigate to a destination in other graph

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)

Categories

Resources