Navigation Component: jump to dynamic root destination and clear backstack - android

When using Jetpack Navigation, we can use popUpTo and popInclusive to clear the stack. But how do I clear the stack when I don't know what destination to popUpto?
Example
Say I have 3 main destinations that should have a clear stack when we arrive at them. And each main destination has its own flow of screens (which can be accessed directly with a deeplink).
Assuming the navigation flows downwards here:
Start app:
- Main Dest 1 (nav to dest 2)
- Dest 2 (nav to dest 3)
- Dest 3 (nav to main dest 2)
--clear--
- (with clear stack)
Main Dest 2 (nav to dest 4)
- Dest 4 (nav to main dest 3)
--clear-->
- (with clear stack)
Main Dest 3
- Back button should close the app here
Since the navigation is very dynamic, I cannot guarantee the root destination. Even though I could, for example, pop up to "Main Dest 2", I cannot know that it's on the stack since I might have gone directly to "dest 4" from a URL.
I there a way of knowing which destination is the lowest so I can pop up to it and clear the stack?

As far as I know there is no easy and beautiful way to get what you're trying to achieve.
There are a few possible solutions though.
Use some sort of a base fragment
You could have a base-destination which is always at the bottom of your stack, let's call it baseDest.
In your actions which need to clear the stack you could then always use
<action
android:id="..."
app:destination="#id/mainDestX"
app:popUpTo="#+id/baseDest"
app:popUpToInclusive="false" />
This will always keep your baseDest at the bottom of your stack allowing you to always popUpTo it.
To achieve your behaviour on a backpress in one of your main destinations you could either:
Override the onBackPressed-method in all your main dests to close the app. (Could be easily done if all your main destionations extend from the same class)
When your baseDest fragment is loaded because of an backpress close the app programmatically.
Rethink your app navigation
Maybe you should rethink how the navigation in your app works.
Rather than having some sort of possibly-cyclic graph structure it's easier to deal with tree-like navigation structures (which are also also more intuitively understood by users). Of course this is not always an option, but you should keep this in mind and consider restructuring your navigation.
Find a solution programmatically
Manually implement all your actions and navigations and use the methods provided in NavController to manually remove all entries in your stack before navigating to one of your main destinations.
One possible approch here to do this is to retrieve your nav controller by using the findNavController method in your fragment.
I have not tested any of the following methods, but you could try to:
Execute yourNavController.getGraph().clear() followed by a yourNavController.navigate(yourDest)
At the beginning of your application before any fragment is loaded use saved = yourNavController.saveState() and on navigation to one of your mainDestinations use yourNavController.restoreState(saved), again followed by a yourNavController.navigate(yourDest)
Create a new NavGraph and use yourNavController.setGraph(createdGraph)

I couldn't find any official solution to the problem, but here's an approach that works for me:
private val mBackStackField by lazy {
val field = NavController::class.java.getDeclaredField("mBackStack")
field.isAccessible = true
field
}
fun popToRoot(navController: NavController) {
val arrayDeque = mBackStackField.get(navController) as java.util.ArrayDeque<NavBackStackEntry>
val graph = arrayDeque.first.destination as NavGraph
val rootDestinationId = graph.startDestination
val navOptions = NavOptions.Builder()
.setPopUpTo(rootDestinationId, false)
.build()
navController.navigate(rootDestinationId, null, navOptions)
}

The root of your graph is always on the back stack, so you can always popUpTo that destination.
So just make sure your root <navigation> element has an android:id associated with it:
<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/main_dest_1">
<!-- The rest of your graph -->
</navigation>
Then whenever you want to pop the entire graph, just popUpTo="#id/nav_graph".
Of course, as per the Principles of Navigation, you should not be popping the whole stack off when you go to Main Dest 2, Main Dest 3, etc. as it is extremely important from a UX perspective that users know when the back button will exit the app - that start destination is exactly that sign post for users to know when that'll happen and prevents accidental closure of the app when users expect to return to the first screen before exiting your app.

You can add a destination change listener in root or base fragment and listen for navigation changes.
NavController.OnDestinationChangedListener { controller, destination, arguments ->
// react on change or
//you can check destination.id or destination.label and act based on tha
)}

Related

NavGraph with dynamic destinations - restore after process kill

This is the setup:
View-based Android UI
Using androidx.navigation library (tested with versions 2.4.1 and 2.5.0-beta01)
Activity, consisting of a bottom bar and a NavHostFragment
What's special is that the navigation structure is defined by the server. The top-level destinations are available during Activity.onCreate(). Other detail screens (=children of the top-level destinations) are only known when performing more server calls while navigating down in the hierarchy.
This is an excerpt of the body of Activity.onCreate():
val navController = findNavController(R.id.navigationHostFragment)
val topLevelDestinations = getTopLevelDestinations()
// some additional destinations are defined statically in navigation.xml (e.g. settings)
val staticNavGraph = navInflater.inflate(R.navigation.navigation)
graph = staticNavGraph.apply {
setStartDestination(topLevelDestinations.first())
addDynamicDestinations(staticNavGraph, topLevelDestinations)
}
This code works initially. The NavGraph contains the top-level destinations.
Some top-level destinations offer navigation to children elements. These destinations are added to the NavGraph in a just-in-time manner, i.e. just before navigate() is called.
When the user navigated to a detail screen, the app process is killed and the app is re-opened, then onCreate() is called again and the app crashes during setGraph()/graph = with the following error:
java.lang.IllegalStateException: Restoring the Navigation back stack failed: destination -1178236840 cannot be found from the current destination Destination(0x1a356ec2) ...
at androidx.navigation.NavController.onGraphCreated(NavController.kt:1128)
at androidx.navigation.NavController.setGraph(NavController.kt:1086)
at androidx.navigation.NavController.setGraph(NavController.kt:100)
To solve this I'd be fine with either of these options:
Find a way, the entire NavGraph is saved persistently and restored
Prevent NavController from trying to recover the last destination, but show the initial start destination again.
Regarding 2. I tried calling navController.popBackStack(startDestination, false) and navController.clearBackStack(startDestination) before setGraph() is called but this doesn't seem to have the desired effect.

How to kill all fragments created by a NavHostFragment?

I have 2 navigation files, and in my Activity, 2 fragments. One of the navigations is always shown inside one of the fragments, but I show the other one only when I need it.
The way they're drawn is the always showing fragment is inside a relativeLayout, and the other fragment is inside the same relativeLayout with it's visibility set as gone. When I need the second navigation, I set the visibility to visible and when I don't need it, I set it to gone again.Visually this works well, but what I want to accomplish is that when I don't want the second navigation, I want to completely kill it and redraw it the next time I need it.
What I've done so far was to get a hold of the NavHostFragment used to start the navigation, and when I dont need it anymore, call popBackStack() on it's navController, but it doesn't work:
val navHost: NavHostFragment? = null
fun createSecondNav() {
navHostLogin = NavHostFragment.create(R.navigation.navigation_second)
theFragment.visibility = View.VISIBLE
supportFragmentManager.beginTransaction()
.replace(R.id.theFragment, navHostLogin!!)
.commit()
}
fun killSecondNav() {
theFragment.visibility = View.GONE
navHostLogin?.navController?.popBackStack() // returns false
navHostLogin = null
}
So how can I completely kill the fragments created by the second navHost?
NavController maintains it's own back-stack, independent form the FragmentManager back-stack.
And popBackStack() without arguments only pops that back-stack once:
Attempts to pop the controller's back stack. Analogous to when the user presses the system Back button when the associated navigation host has focus.
While popBackStack(int destinationId, boolean inclusive) reads:
Attempts to pop the controller's back stack back to a specific destination.
destinationId int: The topmost destination to retain
inclusive boolean: Whether the given destination should also be popped.
So this should be:
navController.popBackStack(R.id.startDestination, true)
I'd wonder why even using two NavController, because one can set the graph at run-time with setGraph(NavGraph graph, Bundle startDestinationArgs):
Sets the navigation graph to the specified graph.
Any current navigation graph data (including back stack) will be replaced.

Android NavController move to another host destination

I have a question regarding to NavController:
There is the scheme (sorry for my painting, hope it helps :D)
I have MainActivity and BottomNavigationView with 3 tabs: A, B, C.
When I tap to A it opens Fragment A1 and there is next button which opens Fragment A2.
In Fragment A2 there are buttons back, next, no problems with navigation here.
The problem is when I need to navigate from Fragment A2 to section B the same like a click on B in BottomNavigationView.
The problem is that it's different graph, how to switch them?
My ideas:
I found work-around: requireActivity().bottomBar.selectedItemId = R.id.graph_b but it's not good idea.
I would like to achieve it using navigation component. I was trying to do findNavController().navigate(R.id.graph_b), but it leads to crash:
java.lang.IllegalArgumentException: navigation destination
com.my.app.staging:id/graph_b is unknown to this NavController
How to make it using NavController ?
There is Google Example project with all architecture:
https://github.com/googlesamples/android-architecture-components/tree/master/NavigationAdvancedSample
And to simplify my question I've added a button in this project, where on click should opens different screen:
You graphs are defined in your Parent Activity and thats where you will be able to control them.
Your first way is actually the solution. Your graphs are defined within your activity. When you are inside any destinations inside your graph A (say A1, A2 etc.) they have no knowledge of the your other graphs B & C. The only way to get to the graph is through parent activity, and hence
requireActivity().bottomBar.selectedItemId = R.id.graph_b
The second way that you have tried will definitely not work because findNavController().navigate(R.id.graph_b) is used when you have nested navigation. In other words graph_b should be inside graph_a which is not your case.
That being said what you can do is just write
requireActivity().bottomBar.selectedItemId = R.id.graph_b
fancier. Instead of running inside your fragments, its better to run inside your activity.
// In your fragment
requireActivity?.moveToGraphB()
// and in your activity
fun moveToGraphB() {
bottomBar.selectedItemId = R.id.graph_b
}
Or more more fancier would be using SharedViewModel which I don't think is necessary.
In graph B you can add the line
where nav_graphA is the name of the graph you want to navigate to when the button is clicked. Then you can add
` <fragment
android:id="#+id/aboutFragment"
android:name="com.mycompany.aboutFragment"
tools:layout="#layout/fragment_about"
android:label="About" >
<action
android:id="#+id/action_aboutFragment_to_nav_graphA"
app:destination="#id/nav_graphA" />
</fragment>
`
to create the action to navigate when the button is clicked.

Android Navigation, I can move any fragment which is not connected

I am trying to use the Navigation architecture component in my toy app.
First I drew the fragments relationship in my "nav_graph.xml".
For example, I drew 3 fragments A, B, and C like below:
A -> B -> C
So I have 2 actions:
action_a_to_b
action_b_to_c
In general, I use the below code to move another fragment.
In A fragment,
findNavController().navigate(ADirections.actionAToB())
In B fragment,
findNavController().navigate(ADirections.actionBToC())
But you may know, there is another way to navigate.
The fragment id can be used to navigate directly like below:
findNavController().navigate(R.id.a)
In my case, I don't have the action for A to C fragment.
But if I use the below code in my A fragment, I can navigate!
findNavController().navigate(R.id.c)
Is it a bug? or intented?
This is intentional as per the documentation for navigate():
This supports both navigating via an action and directly navigating to a destination.
If you're using Safe Args, then only actions are supported. This ensures that you're only using the connections you've specified in your graph.

Navigation Architecture Component - Activities

I've been following the docs from Navigation Architecture Component to understand how this new navigation system works.
To go/back from one screen to another you need a component which implements NavHost interface.
The NavHost is an empty view whereupon destinations are swapped in and
out as a user navigates through your app.
But, it seems that currently only Fragments implement NavHost
The Navigation Architecture Component’s default NavHost implementation is NavHostFragment.
So, my questions are:
Even if I have a very simple screen which can be implemented with an Activity, in order to work with this new navigation system, a Fragment needs to be hosted containing the actual view?
Will Activity implement NavHost interface in a near future?
--UPDATED--
Based on ianhanniballake's answer, I understand that every activity contains its own navigation graph. But if I want to go from one activity to another using the nav component (replacing "old" startActivity call), I can use activity destinations. What is activity destinations is not clear to me because the docs for migration don't go into any detail:
Separate Activities can then be linked by adding activity destinations to the navigation graph, replacing existing usages of startActivity() throughout the code base.
Is there any benefit on using ActivityNavigator instead of startActivity?
What is the proper way to go from activities when using the nav component?
The navigation graph only exists within a single activity. As per the Migrate to Navigation guide, <activity> destinations can be used to start an Activity from within the navigation graph, but once that second activity is started, it is totally separate from the original navigation graph (it could have its own graph or just be a simple activity).
You can add an Activity destination to your navigation graph via the visual editor (by hitting the + button and then selecting an activity in your project) or by manually adding the XML:
<activity
android:id="#+id/secondActivity"
android:name="com.example.SecondActivity" />
Then, you can navigate to that activity (i.e., start the activity) by using it just like any other destination:
Navigation.findNavController(view).navigate(R.id.secondActivity);
I managed to navigate from one activity to another without hosting a Fragment by using ActivityNavigator.
ActivityNavigator(this)
.createDestination()
.setIntent(Intent(this, SecondActivity::class.java))
.navigate(null, null)
I also managed to navigate from one activity to another without hosting a Fragment by using ActivityNavigator.
Kotlin:
val activityNavigator = ActivityNavigator( context!!)
activityNavigator.navigate(
activityNavigator.createDestination().setIntent(
Intent(
context!!,
SecondActivity::class.java
)
), null, null, null
)
Java:
ActivityNavigator activityNavigator = new ActivityNavigator(this);
activityNavigator.navigate(activityNavigator.createDestination().setIntent(new Intent(this, SecondActivity.class)), null, null, null);
nav_graph.xml
<fragment android:id="#+id/fragment"
android:name="com.codingwithmitch.navigationcomponentsexample.SampleFragment"
android:label="fragment_sample"
tools:layout="#layout/fragment_sample">
<action
android:id="#+id/action_confirmationFragment_to_secondActivity"
app:destination="#id/secondActivity" />
</fragment>
<activity
android:id="#+id/secondActivity"
android:name="com.codingwithmitch.navigationcomponentsexample.SecondActivity"
android:label="activity_second"
tools:layout="#layout/activity_second" />
Kotlin:
lateinit var navController: NavController
navController = Navigation.findNavController(view)
navController!!.navigate(R.id.action_confirmationFragment_to_secondActivity)

Categories

Resources