Navigation Component's popUpTo not removing up button - android

I'm using the Navigation Architecture Component and I have a setup similar to this one for popping the stack when navigating to a particular fragment:
<action
android:id="#+id/navigate_to_main_screen"
app:destination="#id/fragment_main_screen"
app:popUpTo="#+id/navigation_main"
app:popUpToInclusive="true"/>
This works almost as expected. Both the system back button and the up icon in the app bar don't navigate to the previous fragment. The system back button exits the app.
However, the up button in the app bar is still there, clicking it doesn't do anything as expected. What am I doing wrong? Why is this still here?
In the main activity I already have
AppBarConfiguration config =
new AppBarConfiguration.Builder(navController.getGraph()).build();
NavigationUI.setupActionBarWithNavController(this, navController, config);
and
#Override
public boolean onSupportNavigateUp() {
return navController.navigateUp() || super.onSupportNavigateUp();
}
As per the documentation.
The library version I'm using:
implementation 'android.arch.navigation:navigation-fragment:1.0.0-alpha09'
implementation 'android.arch.navigation:navigation-ui:1.0.0-alpha09'

If you want to customize which destinations are considered top-level destinations, you can instead pass a set of destination IDs to the constructor, as shown below.
To solve your problem, replace
AppBarConfiguration config =
new AppBarConfiguration.Builder(navController.getGraph()).build();
With
AppBarConfiguration config =
new AppBarConfiguration.Builder(R.id.navigation_main, R.id.fragment_main_screen).build();
More details here: AppBarConfiguration

Related

Navigation Component - Starting destination is incorrect

I started noticing something in my app the other day and its wildly inconsistent. Sometimes it happens and sometimes it doesn't.
I am using the Navigation Component to handle navigation in the app and I started noticing that sometimes, when popping the backstack via the action bar back button or the device back button, it returns to a fragment that is no longer the starting destination (or at least shouldn't be).
In my case the app starts in MainFragment and once authenticated moves to DashboardFragment. This is a pretty common scenario.
Navigation in the app is pretty flat. most of the time its only 1 level deep so nearly all views are accessible from the dashboard.
The app starts at a login view as many do and then to a dashboard where the session will remain as the "start destination". To accomplish this, its done in the nav_graph using popUpTo and popUpToInclusive.
<fragment
android:id="#+id/mainFragment"
android:name="com.example.view.fragments.MainFragment"
android:label="Welcome">
<action
android:id="#+id/action_mainFragment_to_dashboardFragment"
app:destination="#id/dashboardFragment"
app:popUpTo="#id/mainFragment"
app:popUpToInclusive="true"/>
</fragment>
<fragment
android:id="#+id/dashboardFragment"
android:name="com.example.view.fragments.dashboard.DashboardFragment"
android:label="#string/dashboard_header" >
<action
android:id="#+id/action_dashboardFragment_to_notificationsFragment"
app:destination="#id/notificationsFragment" />
</fragment>
When the user successfully authenticates and its time to go to the dashboard, I use NavController.navigate() to send them there.
findNavController().navigate(
MainFragmentDirections.actionMainFragmentToDashboardFragment()
)
// This should have the same result and it does appear to be affected by the same issue
// findNavController().navigate(R.id.action_mainFragment_to_dashboardFragment)
I have an action bar with a back arrow and a navigation drawer. In the main activity I need to define the AppBarConfiguration and override onSupportNavigateUp()
lateinit var appBarConfiguration: AppBarConfiguration
...
override fun onCreate(savedInstanceState: Bundle?) {
Timber.d("onCreate()")
super.onCreate(savedInstanceState)
_binding = ActivityMainBinding.inflate(layoutInflater)
setContentView(binding.root)
// There is 2 different drawer menu's respectfully.
appBarConfiguration = AppBarConfiguration(
setOf(
R.id.mainFragment,
R.id.dashboardFragment
), binding.drawerLayout
)
setSupportActionBar(binding.toolbar)
setupActionBarWithNavController(navController, appBarConfiguration)
}
...
override fun onSupportNavigateUp(): Boolean {
Timber.d("-> onSupportNavigateUp()")
val breadcrumb = navController
.backStack
.map { it.destination }
.filterNot { it is NavGraph }
.joinToString(" > ") { it.displayName.split('/')[1] }
Timber.d("Backstack: $breadcrumb")
Timber.d("Previous backstack entry: ${navController.previousBackStackEntry?.destination?.displayName}")
return navController.navigateUp(appBarConfiguration) || super.onSupportNavigateUp()
}
...
The logs look like this when we step back and it is working correctly
D/MainActivity: -> onSupportNavigateUp()
D/MainActivity: Backstack: dashboardFragment > testingFragment
D/MainActivity: Previous backstack entry: com.example:id/dashboardFragment
D/DashboardFragment: -> onCreateView()
D/BaseFragment: -> onCreateView()
D/DashboardFragment: -> onViewCreated()
I also noticed when using the hamburger in the action bar that it also calls onSupportNavigateUp()
D/MainActivity: -> onSupportNavigateUp()
D/MainActivity: pendingAction: false
D/MainActivity: Backstack: dashboardFragment
D/MainActivity: Previous backstack entry: null
When I use the drawer to navigate to a destination I do see this in the logs and im not sure where/why this is returned or if it has any importance
I/NavController: Ignoring popBackStack to destination com.example:id/mainFragment as it was not found on the current back stack
Now, when its NOT working correctly, this is what the logs look like
D/MainActivity: -> onSupportNavigateUp()
D/MainActivity: Backstack: mainFragment > testingFragment
D/MainActivity: Previous backstack entry: com.example:id/mainFragment
D/MainFragment: -> onCreateView()
D/BaseFragment: -> onCreateView()
D/MainFragment: -> onViewCreated()
This really feels like the popUpTo and popUpToInclusive properties are not being applied (sometimes) when performing the navigation from main fragment to dashboard fragment. It's also suspicious that even though the dashboard fragment is not set as the new starting destination but also its missing from the backstack. Assuming the properties were NOT applied I would expected to see the breadcrumb Backstack: mainFragment > dashboardFragment > testingFragment
Any help would be greatly appreciated!
While there may certainly be a better procedure to follow (as outlined in the Principles of Navigation in the previous comments) the overhead of the change introduces a plethora of new errors and the scope is too large at this time.
Its still unknown why popUpTo and popUpToInclusive are not reliable via XML when navigating (even using NavDirections) however, thus far, passing the NavOptions while navigating seems to resolve the issue.
findNavController().navigate(
MainFragmentDirections.actionMainFragmentToDashboardFragment(),
NavOptions.Builder().setPopUpTo(R.id.mainFragment, true).build()
)
Thus far the issue has yet to arise again.
As it says in Ian's first link:
The back stack always has the start destination of the app at the bottom of the stack.
Your start destination is the "home" one in a navigation graph, the one identified with the little house icon. Set with the startDestination attribute in the XML. When the Navigation library creates a backstack, it always has that destination at the bottom. And it will always be there, even if you try to avoid it with the poUpTo attributes.
That's why if there's a fragment you consider your "home" one, like your dashboard, that needs to explicitly be your startDestination. It's the last thing a user will see if they back out of your app. If you have something like a login or welcome screen as the start destination, they'll back out to that.
Which is why you need to set the "home" fragment as the start destination, and then handle any extra navigation to a login or welcome screen from there. It's just how the Navigation stuff is designed to work. If you try and work around it (I did!) you'll run into other problems, and a lot of the nice features like automatic backstack recreation might not work properly

Navigation Controller (Managing Backstack) Jetpack Android

Good day. So I've been working around with NavComponent of Jetpack for Android
I've thought that management of BackStack of fragments had to be implemented there already, well in fact it is there but I have faced an issue.
Here is my structure:
I have and entry Activity
I have a NavHost in the activity
I have Bottom Navigation bar in the Activity
For each Bottom Item I am using separate Fragments to navigate through.
Here is the code for the navigation.
bottomNavigationView.setOnNavigationItemSelectedListener {
when (it.itemId) {
R.id.navigation_home -> {
navController.apply {
navigate(R.id.navigation_home)
}
true
}
R.id.navigation_dashboard -> {
navController.apply {
navigate(R.id.dashboardFragment)
}
true
}
R.id.navigation_notifications -> {
true
}
else -> {
false
}
}
}
Never mind the last item.
So the issue is next.
If I try to switch between home and dashboard multiple times, when I press back then the stack surely will start popping all the items included there. So if I move like 6 times it will take me 12 attempts to actually exit the app.
Currently I couldn't find any source where for example the navigate() method will accept some sort of argument to cash my fragments instead of recreating it each time and adding to the BackStack.
So what kind of approach would you suggest?
If I to manage the BackStack manually on each back button pressed, what's the purpose of NavController at all? Just for creating and FORWARD navigation?
I think I'm missing some source in Android's official docs.
Thank you beforehand.
P.S.
using navController.popBackStack() before calling navigate() surely isn't the correct choice.
According to the documentation here :
NavigationUI can also handle bottom navigation. When a user selects a menu item, the NavController calls onNavDestinationSelected() and automatically updates the selected item in the bottom navigation bar.
to do so you have to give your bottom navigation items an ids as same as the corresponding destination in your navigation graph , and then tie you bottom view to the controller like this :
NavHostFragment navHostFragment = supportFragmentManager.findFragmentById(R.id.nav_host_fragment);
NavController navController = navHostFragment.getNavController();
BottomNavigationView bottomNav = findViewById(R.id.bottom_nav);
NavigationUI.setupWithNavController(bottomNav, navController);
Note : from my personal experience , when the startDestination in the graph , that start by default is not currently in back stack (In my case it was the landing page which i pop it out when going to home fragment) then the app act with weird behavior like this . so make sure the start destination is existed in your back stack on should work fine .

NavController crashes upon orientation change

I'm not really sure what I'm doing wrong here. Simple setup, single activity with fragments controlled by a bottom bar inside, so far so good. The start fragment has a button inside which should navigate to another fragment.
Here's the onclicklistener for the button inside my fragment class:
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
add_step_fab.setOnClickListener {
Navigation.findNavController(view).navigate(R.id.fragmentAtoB)
}
}
Here's my action in the navigation.xml:
<fragment android:id="#+id/navigation_main" android:name="com.test.test.fragments.AFragment"
android:label="Fragment A" tools:layout="#layout/fragment_a">
<action android:id="#+id/fragmentAtoB"
app:destination="#id/fragmentB" 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>
The navigation works, but I get the standard
java.lang.IllegalArgumentException: navigation destination com.test.test:id/fragmentAtoB is unknown to this NavController
error when I click on the button after rotating my screen, split screening the app, or sometimes seemingly randomly. Looks like it has something to do with the configuration change?
EDIT:
I also tried using
val clickListener: View.OnClickListener = Navigation.createNavigateOnClickListener(R.id.fragmentAtoB)
add_step_fab.setOnClickListener(clickListener)
with the same issue as above.
Checking if i'm actually in the same fragment, as some suggested for users who are having the same exception (but for unrelated reasons), such as:
if (controller.currentDestination?.id == R.id.navigation_main) {
controller.navigate(R.id.fragmentAtoB)
}
also didn't help.
I also experienced this recently after months of an app working correctly. I originally had added navigation to an app that was utilizing a "MainActivity" class and then had other activities that would be launched from a menu (as opposed to having the Navigation component navigate between fragments off of a single Activity).
So the app is organized such that the MainActivity is the "hub", and choices from menu options cause other activities to be launched, and when the user returns from them they are back at the MainActivity - so a pure "hub (MainActivity) and spoke (5 other activities)" model.
Within MainActivity.onCreate() is called a setupNavigation() method which originally looked like:
private void setupNavigation() {
Toolbar toolbar = findViewById(R.id.toolbar);
setSupportActionBar(toolbar);
drawerLayout = findViewById(R.id.drawer_layout);
// This class implements the listener interface for the NavigationView, handler is below.
NavigationView navigationView = findViewById(R.id.nav_view);
navController = Navigation.findNavController(this, R.id.nav_host_fragment);
NavigationUI.setupActionBarWithNavController(this, navController, drawerLayout);
NavigationUI.setupWithNavController(navigationView, navController);
// MainActivity implements the listener for navigation events
navigationView.setNavigationItemSelectedListener(this);
}
As I mentioned, this was working just fine even with configuration changes until recently. Even just changing the configuration from portrait to landscape on the MainActivity would cause an exception:
ComponentInfo{com.reddragon.intouch/com.reddragon.intouch.ui.MainActivity}: java.lang.IllegalStateException: You must call setGraph() before calling getGraph()
I found I had to add this line:
navController.setGraph(R.navigation.nav_host);
Right after the Naviation.findNavController() call, and then it handled configuration changes just fine.
After a configuration change, the fragment or activity goes out of scope temporarily and then executes an onResume() override. If you have anything which is initialized in onCreate(), or onViewCreated(), then those variables may have gone out of scope and are now null values. So, I usually have a method called RestoreAll() which is responsible for instantiating all of the objects used by the code, and I call this method from onResume() so that every time there is a configuration change, all the local objects are re-instantiated.
There is a problem with nav controller backstack after changed config. Try this:
https://stackoverflow.com/a/59987336/5433148

Login - Navigation Architecture Component

I implemented conditional navigation to my LoginFragment, with android navigation architecture component. The problem I facing now, is that I would like to hide the up button on the toolbar, and the disable any in-app navigation while the user is not logged in.
I would like to be able to implement this with a one-activity approach, where the Activity sets up the in app navigation UI and the navController like in the android sunflower demo, and the navigation destinations are Fragments.
I implemented the conditional navigation as discribed here:
Navigation Architecture Component - Login screen
How can I properly implement hiding the navigation and the up button on the login screen, with Navigation Architecture Component?
I don't know exactly what you mean by hiding navigation, but I will assume you mean hiding a drawer layout. To hide the up button and lock the drawer add the following to your MainActivity's onCreate. I'm using Kotlin.
myNavController.addOnDestinationChangedListener { _, destination ->
if (destination.id == R.id.loginFragment) {
myDrawerLayout.setDrawerLockMode(DrawerLayout.LOCK_MODE_LOCKED_CLOSED)
myToolbar.setVisibility(View.GONE)
} else {
myDrawerLayout.setDrawerLockMode(DrawerLayout.LOCK_MODE_UNLOCKED)
myToolbar.setVisibility(View.VISIBLE)
}
To make just the up button go away use myToolbar.setNavigationIcon(null) and to make it come back use myToolbar.setNavigationIcon(R.id.my_icon)
My method is adding the login page to the root set
val navController = findNavController(R.id.main_nav_host)
val appBarConfiguration = AppBarConfiguration(setOf(R.id.home_dest,
R.id.user_dest,R.id.login_dest))
toolbar.setupWithNavController(navController, appBarConfiguration)
So when you are on the login page, there is no back button.
System back button can override onBackPressed()
override fun onBackPressed() {
if (findNavController(R.id.main_nav_host).currentDestination?.id != R.id.next_dest)
super.onBackPressed()
}
}
Sorry for my English

Android Navigation Architecture Component - Nav Drawer Icons

I'm currently using the Android Architecture Component's Navigation, but I'm running into an issue with my Navigation Drawer. It shows the hamburger menu when at my starting destination, but other Fragments are showing the up arrow. I believe I've setup my navigation_graph incorrectly.
Here you can see my nav drawer, showing 2 items, Home and Settings. When in either of these Fragments, you should see the Hamburger icon.
However, when navigating to the Settings Fragment, it shows the Up arrow.
navigation.graph.xml
<?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"
app:startDestination="#id/nav_home">
<!-- Start at HomeFragment -->
<fragment
android:id="#+id/nav_home"
android:name=".HomeFragment"
android:label="#string/home">
<!-- Navigate to the Search -->
<action
android:id="#+id/action_nav_home_to_nav_search"
app:destination="#id/nav_search" />
</fragment>
<fragment
android:id="#+id/nav_settings"
android:name=".SettingsFragment"
android:label="#string/settings">
<!-- Navigate to the Search -->
<action
android:id="#+id/action_nav_settings_to_nav_search"
app:destination="#id/nav_search" />
</fragment>
<fragment
android:id="#+id/nav_search"
android:name=".SearchFragment"
android:label="#string/search" />
</navigation>
I feel like HomeFragment and SettingsFragment should be related somehow but I'm not sure how to define that.
main_drawer.xml
<?xml version="1.0" encoding="utf-8"?>
<menu xmlns:android="http://schemas.android.com/apk/res/android">
<group android:checkableBehavior="single">
<item
android:id="#id/nav_home"
android:icon="#drawable/ic_home_white_24dp"
android:title="#string/home" />
<item
android:id="#id/nav_settings"
android:icon="#drawable/ic_settings_white_24dp"
android:title="#string/settings" />
</group>
</menu>
MainActivity
And then within MainActivity, I just set it up like this. I called setupActionBarWithNavController, but I also have to actually setup the nav drawer myself, and handle the onNavigationItemSelected.
private fun setupNavigation() {
navController = findNavController(R.id.mainNavigationFragment)
setupActionBarWithNavController(this, navController, drawer_layout)
val toggle = ActionBarDrawerToggle(
this, drawer_layout, toolbar, R.string.navigation_drawer_open, R.string.navigation_drawer_close)
drawer_layout.addDrawerListener(toggle)
toggle.syncState()
nav_view.setNavigationItemSelectedListener(this)
}
override fun onNavigationItemSelected(item: MenuItem): Boolean {
val current = navController.currentDestination.id
if (item.itemId != current) {
navController.navigate(item.itemId)
}
drawer_layout.closeDrawers()
return true
}
build.gradle
// Navigation
implementation 'android.arch.navigation:navigation-fragment-ktx:1.0.0-alpha04'
implementation 'android.arch.navigation:navigation-ui-ktx:1.0.0-alpha04'
Thanks.
In newer alphas (I have 1.0.0-alpha07) they added possibility to define topLevelDestinationIds when calling AppBarConfiguration constructor.
So I setup my NavController like this
val navController = findNavController(R.id.nav_host_fragment)
val config = AppBarConfiguration(
setOf(
R.id.fistTopFragment,
R.id.secondTopFragment,
...
),
dr.drawerLayout
)
tb.setupWithNavController(navController, config)
Where dr is MaterialDrawer and tb of course Toolbar.
Then it behaves more like Gmail, at least for the ActionBarDrawerToggle, the back stack is still preserved. Since I must handle item selection in MaterialDrawer by myself, I'm going to reduce back stack actions using global navigation actions with inclusive popTo to the root fragment of the navigation graph and use something like a "Welcome screen" for now.
Another way around could be custom handling of the onBackPressed.
You must remove app:defaultNavHost="true" from your host fragment in activity layout first. Something like this
override fun onBackPressed() {
val navController = findNavController(R.id.nav_host_fragment)
if (navController.currentDestination == null
|| navController.currentDestination!!.id in setOf(
R.id.fistTopFragment,
R.id.secondTopFragment,
...
)
) {
super.onBackPressed()
} else {
navController.navigateUp()
}
}
Sorry about the code, I'm still learning Kotlin, so there is probably much nicer way of doing this.
I am afraid it is a feature of navigation component.
The action bar will also display the Up button when you are on a non-root destination and the drawer icon when on the root destination, automatically animating between them.
If you want, you can try to use setupWithNavController(NavigationView, NavController) and handle the toolbar yourself.
I made simple example for this issue. Solution is almost same as Almighty's answer. https://github.com/isaul32/android-sunflower
Create set of top level destinations at first
val topLevelDestinations = setOf(R.id.garden_fragment,
R.id.plant_list_fragment)
appBarConfiguration = AppBarConfiguration.Builder(topLevelDestinations)
.setDrawerLayout(drawerLayout)
.build()
and then override onSupportNavigateUp function like this
override fun onSupportNavigateUp(): Boolean {
return NavigationUI.navigateUp(navController, appBarConfiguration)
}
Back arrow appearing on tab fragments associated with BottomNavigationView is an intended behaviour. However haven't seen it being used "as is" even in famous apps (Instagram, Youtube which have bottom tabs). If you navigate to different bottom tabs in Youtube app for example and click device back option, you'll notice it goes to previous tab fragment and not exit app. So bottomnavigationview tab fragments are not root destinations here.
Want to bring to notice additional important issues which you might encounter as you move forward:
It does not allow for reuse of fragments in combination with BottomNavigationView
https://issuetracker.google.com/issues/110373186
If you have multiple activities the up button does not navigate up to the previous activity
https://issuetracker.google.com/issues/79993862
However there are several hooks you can use to customise the behaviour:
onBackPressed - to close drawer layout if open
override fun onBackPressed() {
if (drawerLayout.isDrawerOpen(GravityCompat.START)) {
drawerLayout.closeDrawer(GravityCompat.START)
} else {
super.onBackPressed()
}
}
Add navController.addOnNavigatedListener(..) and inside the listener customise HomeAsUpIndicator icon
Override onOptionsItemSelected to customise menu with id android.R.id.home action
Set custom fragment navigator to customise how your fragments are treated (replace or show/ hide)
navHostFragment = supportFragmentManager
.findFragmentById(R.id.my_nav_host_fragment) as NavHostFragment? ?: return
val customNavigator = CustomFragmentNavigator(navHostFragment.requireContext(),
navHostFragment.childFragmentManager, navHostFragment.id)
navHostFragment.navController.navigatorProvider.addNavigator(customNavigator)
val inflater = navHostFragment.navController.navInflater
val graph = inflater.inflate(R.navigation.main_nav_graph)
navHostFragment.navController.graph = graph
Remember navigation arch component is still in alpha, so use it wisely.
If you want, you can use NavController.OnNavigatedListener and use below code to set title.
#Override
public void onNavigated(#NonNull NavController controller, #NonNull NavDestination destination) {
mActivityBinding.toolbar.setTitle(destination.getLabel());
}
Remember to set label in your navigation graph.
I think your menu items should be like this:
<?xml version="1.0" encoding="utf-8"?>
<menu xmlns:android="http://schemas.android.com/apk/res/android">
<item
android:id="#id/nav_home"
android:icon="#drawable/ic_home_white_24dp"
android:title="#string/home" />
<item
android:id="#id/nav_settings"
android:icon="#drawable/ic_settings_white_24dp"
android:title="#string/settings" />
</menu>
Hope this helps
Happy coding...
I completely agree with the sentiment here but it is a part of the library
setupActionBarWithNavController
Sets up the ActionBar returned by AppCompatActivity.getSupportActionBar() for use with a NavController.
By calling this method, the title in the action bar will automatically be updated when the destination changes (assuming there is a valid label).
The start destination of your navigation graph is considered the only top level destination. On the start destination of your navigation graph, the ActionBar will show the drawer icon if the given DrawerLayout is non null. On all other destinations, the ActionBar will show the Up button. Call navigateUp(NavController, DrawerLayout) to handle the Up button.
this to me is broken and not the desired result, so what i'm doing is still using it with the navigation view using
navController = Navigation.findNavController(this, R.id.nav_host);
NavigationUI.setupWithNavController(navigationView,navController);
and then having a listener to update the title and anything else i want changed like this
navController.addOnNavigatedListener((controller, destination) -> {
setToolbarColour(R.color.primary);
switch (destination.getId()){
case R.id.dashBoard :
setToolbarColour(android.R.color.transparent);
break;
case R.id.requests :
toolbar.setTitle(getString(R.string.request));
break;
}
});
I'm then creating a new navigation graph for each of these fragments, not great but i get my intended result

Categories

Resources