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.
Related
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
)}
I hope you're all doing well.
I have a problem about navigation graphs. You can see the structure in the image below;
So we have 2 different navigation graphs; navigation_A and navigation_B.
I need to navigate to Fragment Y and Fragment Z from a fragment in nested graph which is navigation_B.
The problem is;
If I use <include> for navigation_B, I can not use it as NavHost.
If I use different navigation graph and add it as nested graph like above, I can not use navigation destination for Fragment Y or Fragment Z.
I need to use navigation_B as NavHost, also i need to be able to navigate to Fragment Y and Fragment Z. How can I achieve that?
Thank you!
Your navigation graph structure breaks the recommended navigation pattern!
But for your needs, I have a way to workaround.
Method 1:
First, FragmentY and FragmentZ need to have their own deeplinks.
<fragment
android:id="#+id/fragY"
android:name=".FragmentY"
tools:layout="#layout/frag_y">
<deepLink
android:id="#+id/"
app:uri="any-app://internal_navigation_frag_y" />
</fragment>
<fragment
android:id="#+id/fragZ"
android:name=".FragmentZ"
tools:layout="#layout/frag_z">
<deepLink
android:id="#+id/dlink_frag_z"
app:uri="any-app://internal_navigation_frag_z" />
</fragment>
Then, Inside a Fragment that lives in navigation_b. Let's call it FragmentB
// navigate to FramgmentY
val deeplinkY = Uri.parse("any-app://internal_navigation_frag_y")
findNavController().navigate(deeplinkY)
// navigate to FramgmentZ
val deeplinkZ = Uri.parse("any-app://internal_navigation_frag_z")
findNavController().navigate(deeplinkZ)
You can store the string any-app://internal_navigation_frag_y and any-app://internal_navigation_frag_z in string.xml file though.
Check this out: https://developer.android.com/guide/navigation/navigation-navigate#uri
Method 2:
Inside the nested graph navigation_B, define 2 global actions that point to fragmentY and fragmentZ. Since navigation_B is a NavHost, so it will know about FragmentY and FragmnentZ
Check this: https://developer.android.com/guide/navigation/navigation-global-action
When you use nested navigation graph the inner graph should not know anything about the outside graph. In this case graph_B should not know about the existence of a FragmentY or FragmentZ. What you should do is deliver a result from your graph_B to whomever launched it. Then at that point decide whether to go FragmentY or FragmentZ.
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.
I am trying to implement navigation to specific Detail pages of my app using PendingIntent from a notification, however I am having problems recreating the backstack from the Detail page all the way back to the start destination.
I made a sample app here with a single activity and three fragments to demo this:
Fragment 1 -> Fragment 2 -> Fragment 3
(start dest) <- <-
From Fragment 1 (the start destination), I navigate directly to Fragment 3 using
Navigation.findNavController(this, R.id.nav_host_fragment).navigate(R.id.fragment2,
null,
NavOptions.Builder()
.build())
From Fragment 3, when I call Navigation.findNavController(this,R.id.nav_host_fragment).navigateUp() I am navigated back to Fragment 1. Is there a way to get this to navigate to a newly created Fragment 2 instead?
Thanks to M.G for pointing to the right direction, I have managed to solve this by manually creating the backstack using the navigation library. This means sequentially calling findNavController(...).navigate(...) multiple times to create a backstack.
For example when I deep link to fragment 3 but want an up navigation back to fragments 1 and 2, I call:
findNavController.navigate(R.id.fragment1, ...)
findNavController.navigate(R.id.fragment2, ...)
findNavController.navigate(R.id.fragment3, ...)
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)