Android architecture navigation popBackStack throws IllegalArgumentException "back stack is empty" - android

I have an app that's using the new navigation library. I've attached a listener to the NavController and here's what it looks like as I navigate through the app:
main_fragment
authentication_subgraph
loading_fragment
set_username_fragment calls popBackStack(R.id.authentication, true)
loading_fragment removed as a part of the pop back stack above.
main_fragment
squad_fragment
In some cases reported to me through Crashlytics I see that a call from squad_fragment throws an IllegalArgumentException, the call looks like:
close.setOnClickListener {
if (isResumed) {
findNavController().popBackStack()
}
}
This should and in every test I've run simply pop squad_fragment and leave me with main_fragment, but in some cases (20 times last night) I got an exception here saying that the back stack is empty. How can this be? Anything I'm doing wrong?
UPDATE
I've been able to replicate the issue finally. This appear to happen when I navigate anywhere from main_fragment then background the app, do a bunch of other things, then return to the app from the recents list. It would seem that the navigation library doesn't restore the back stack? I'm using version 1.0.0-alpha02 right now. It appears that if instead of calling popBackStack() I call navigateUp() it works, but this concerns me because the way I exit my authentication subgraph currently is by calling popBackStack(R.id.authentication, true) in order to return to whatever fragment triggered the auth flow. This will eventually break in the same manner.
Looking at the code in NavController it appears that the back stack is saved and restored. So I don't understand why the backstack comes back as empty. Additionally, NavHostFragment appears to be backing up the NavController state as well as recovering it so I don't know why my backstack is empty.

Related

How to avoid "navigation XYZ cannot be found from the current destination"

Some crashes have popped out on my Firebase's crashlytics with the exception
Fatal Exception: java.lang.IllegalArgumentException Navigation ACTION_XYZ cannot be found from the current destination
I've debugged this case and found out the problem:
I have a button which navigates from Fragment A to Fragment B and it works good.
But when you quickly click the button two times - first it navigates correctly, then tries to navigate again, thus the exception.
How should one avoid such a bug? I could just silently catch the exception from the button's click but that looks like a code smell to me.
I also could disable the button after the first click, but I'm wondering if there is a more elegant and cleaner way to avoid double navigation to FragmentB?
Thanks for all the answers, cheers!
Before navigating to any screen, you can check whether we have action to that particular destination from the current screen. Something like below. You can use this extension function to navigate safely to any destination.
fun NavController.navigateSafe(directions: NavDirections, navOptions: NavOptions? = null) {
currentDestination?.getAction(directions.actionId)?.let {
navigate(directions, navOptions)
}
}

Navigation: Multiple back stacks without losing view state, any saved instance state and ViewModel attached to that fragment

When we call popBackStack() (either directly or via FragmentManager’s integration with the system back button), the topmost transaction on the fragment back stack is reversed — an added fragment is removed, a hidden fragment is shown, etc. This puts the FragmentManager back into the same state that it was before the fragment transaction was initially committed.
This means that popBackStack() is a destructive operation: any added fragment will have its state destroyed when that transaction is popped. This means you lose your view state, any saved instance state, and any ViewModel instances you’ve attached to that fragment are cleared. How to resolve this problem?
Android has published article and implemented new mechanism to handle this scenario.
From https://developer.android.com/news an article published on 7th June 2021 https://medium.com/androiddevelopers/multiple-back-stacks-b714d974f134
Multiple back stacks. A deep dive
If a ‘back stack’ is a set of screens that you can navigate back through via the system back button, ‘multiple back stacks’ is just a bunch of those, right? Well, that’s exactly what we’ve done with the multiple back stack support added in Navigation 2.4.0-alpha01 and Fragment 1.4.0-alpha01!
The joys of the system back button
Whether you’re using Android’s new gesture navigation system or the traditional navigation bar, the ability for users to go ‘back’ is a key part to the user experience on Android and doing that right is an important part to making your app feel like a natural part of the ecosystem.
In the simplest cases, the system back button just finishes your activity. While in the past you might have been tempted to override the onBackPressed() method of your activity to customize this behavior, it is 2021 and that is totally unnecessary. Instead, there are APIs for custom back navigation in the OnBackPressedDispatcher. This is actually the same API that FragmentManager and NavController already plug into.
That means when you use either Fragments or Navigation, they use the OnBackPressedDispatcher to ensure that if you’re using their back stack APIs, the system back button works to reverse each of the screens that you’ve pushed onto the back stack.
Multiple back stacks doesn’t change these fundamentals. The system back button is still a one directional command — ‘go back’. This has a profound effect on how the multiple back stack APIs work.
Multiple back stacks in Fragments
At the surface level, the support for multiple back stacks is deceptively straightforward, but requires a bit of an explanation of what actually is the ‘fragment back stack’. The FragmentManager’s back stack isn’t made up of fragments, but instead is made up of fragment transactions. Specifically, the ones that have used the addToBackStack(String name) API.
This means when you commit() a fragment transaction with addToBackStack(), the FragmentManager is going to execute the transaction by going through and executing each of the operations (the replace, etc.) that you specified on the transaction, thus moving each fragment through to its expected state. FragmentManager then holds onto that transaction as part of its back stack.
When you call popBackStack() (either directly or via FragmentManager’s integration with the system back button), the topmost transaction on the fragment back stack is reversed — an added fragment is removed, a hidden fragment is shown, etc. This puts the FragmentManager back into the same state that it was before the fragment transaction was initially committed.
Note: I cannot stress this enough, but you absolutely should never interleave transactions with addToBackStack() and transactions without in the same FragmentManager: transactions on your back stack are blissfully unaware of non-back stack changing fragment transactions — swapping things out from underneath those transactions makes that reversal when you pop a much more dicey proposition.
This means that popBackStack() is a destructive operation: any added fragment will have its state destroyed when that transaction is popped. This means you lose your view state, any saved instance state, and any ViewModel instances you’ve attached to that fragment are cleared. This is the main difference between that API and the new saveBackStack(). saveBackStack() does the same reversal that popping the transaction does, but it ensures that the view state, saved instance state, and ViewModel instances are all saved from destruction. This is how the restoreBackStack() API can later recreate those transactions and their fragments from the saved state and effectively ‘redo’ everything that was saved. Magic!
This didn’t come without paying down a lot of technical debt though.
Paying down our technical debts in Fragments
While fragments have always saved the Fragment’s view state, the only time that a fragment’s onSaveInstanceState() would be called would be when the Activity’s onSaveInstanceState() was called. To ensure that the saved instance state is saved when calling saveBackStack(), we need to also inject a call to onSaveInstanceState() at the right point in the fragment lifecycle transitions. We can’t call it too soon (your fragment should never have its state saved while it is still STARTED), but not too late (you want to save the state before the fragment is destroyed).
This requirement kicked off a process to fix how FragmentManager moves to state to make sure there’s one place that manages moving a fragment to its expected state and handles re-entrant behavior and all the state transitions that go into fragments.
35 changes and 6 months into that restructuring of fragments, it turned out that postponed fragments were seriously broken, leading to a world where postponed transactions were left floating in limbo — not actually committed and not actually not committed. Over 65 changes and another 5 months later, and we had completely rewritten most of the internals of how FragmentManager manages state, postponed transitions, and animations.
What to expect in Fragments
With the technical debt paid down (and a much more reliable and understandable FragmentManager), the tip of the iceberg APIs of saveBackStack() and restoreBackStack() were added.
If you don’t use these new APIs, nothing changes: the single FragmentManager back stack works as before. The existing addToBackStack() API remains unchanged — you can use a null name or any name you want. However, that name takes on a new importance when you start looking at multiple back stacks: it is that name that is the unique key for that fragment transaction that you’d use with saveBackStack() and later with restoreBackStack().
This might be easier to see in an example. Let’s say that you have added an initial fragment to your activity, then done two transactions, each with a single replace operation:
// This is the initial fragment the user sees
fragmentManager.commit {
setReorderingAllowed(true)
replace<HomeFragment>(R.id.fragment_container)
}
// Later, in response to user actions, we’ve added two more
// transactions to the back stack
fragmentManager.commit {
setReorderingAllowed(true)
replace<ProfileFragment>(R.id.fragment_container)
addToBackStack(“profile”)
}
fragmentManager.commit {
setReorderingAllowed(true)
replace<EditProfileFragment>(R.id.fragment_container)
addToBackStack(“edit_profile”)
}
This means that our FragmentManager looks like:
Let’s say that we want to swap out our profile back stack and swap to the notifications fragment. We’d call saveBackStack() followed by a new transaction:
fragmentManager.saveBackStack("profile")
fragmentManager.commit {
setReorderingAllowed(true)
replace<NotificationsFragment>(R.id.fragment_container)
addToBackStack("notifications")
}
Now our transaction that added the ProfileFragment and the transaction that added the EditProfileFragment has been saved under the "profile"
key. Those fragments have had their state saved completely and FragmentManager is holding onto their state alongside the transaction state. Importantly: those fragment instances no longer exist in memory or in the FragmentManager — it is just the state (and any non config state in the form of ViewModel instances):
Swapping back is simple enough: we can do the same saveBackStack() operation on our "notifications" transaction and then restoreBackStack():
fragmentManager.saveBackStack(“notifications”)
fragmentManager.restoreBackStack(“profile”)
The two stacks have effectively swapped positions:
This style of maintaining a single active back stack and swapping transactions onto it ensures that the FragmentManager and the rest of the system always has a consistent view of what actually is supposed to happen when the system back button is tapped. In fact, that logic remained entirely unchanged: it still just pops the last transaction off of the fragment back stack like before.
These APIs are purposefully minimal, despite their underlying effects. This makes it possible to build your own structure on top of these building blocks while avoiding any hacks to save Fragment view state, saved instance state, and non config state.
Of course, if you don’t want to build your own structure on top of these APIs, you can also use the one we provide.
Bringing multiple back stacks to any screen type with Navigation
The Navigation Component was built from the beginning as a generic runtime that knows nothing about Views, Fragments, Composables, or any other type of screen or ‘destination’ you might implement within your activity. Instead, it is the responsibility of an implementation of the NavHost interface to add one or more Navigator instances that do know how to interact with a particular type of destination.
This meant that the logic for interacting with fragments was entirely encapsulated in the navigation-fragment artifact and its FragmentNavigator and DialogFragmentNavigator. Similarly the logic for interacting with Composables is in the completely independent navigation-compose artifact and its ComposeNavigator. That abstraction means that if you want to build your app solely with Composables, you are not forced to pull in any dependency on fragments when you use Navigation Compose.
This level of separation means that there are really two layers to multiple back stacks in Navigation:
Saving the state of the individual NavBackStackEntry instances that make up the NavController back stack. This is the responsibility of the NavController.
Saving any Navigator specific state associated with each NavBackStackEntry (e.g., the fragment associated with a FragmentNavigator destination). This is the responsibility of the Navigator.
Special attention was given to the cases where the Navigator has not been updated to support saving its state. While the underlying Navigator API was entirely rewritten to support saving state (with new overloads of its navigate() and popBackStack() APIs that you should override instead of the previous versions), NavController will save the NavBackStackEntry state even if the Navigator has not been updated (backward compatibility is a big deal in the Jetpack world!).
PS: this new Navigator API also makes it way easier to test your own custom Navigator in isolation by attaching a TestNavigatorState that acts as a mini-NavController.
If you’re just using Navigation in your app, the Navigator level is more of an implementation detail than something you’ll ever need to interact with directly. Suffice it to say, we’ve already done the work required to get the FragmentNavigator and the ComposeNavigator over to the new Navigator APIs so that they correctly save and restore their state; there’s no work you need to do at that level.
Enabling multiple back stacks in Navigation
If you’re using NavigationUI, our set of opinionated helpers for connecting your NavController to Material view components, you’ll find that multiple back stacks is enabled by default for menu items, BottomNavigationView (and now NavigationRailView!), and NavigationView. This means that the common combination of using navigation-fragment and navigation-ui will just work.
The NavigationUI APIs are purposefully built on top of the other public APIs available in Navigation, ensuring that you can build your own versions for precisely your set of custom components you want. The APIs to enable saving and restoring a back stack are no exception to this, with new APIs on NavOptions, the navOptions Kotlin DSL, in the Navigation XML, and in an overload for popBackStack() that let you specify that you want a pop operation to save state or you want a navigate operation to restore some previously saved state.
For example, in Compose, any global navigation pattern (whether it is a bottom navigation bar, navigation rail, drawer, or anything you can dream up) can all use the same technique as we show for integrating with BottomNavigation and call navigate() with the saveState and restoreState attributes:
onClick = {
navController.navigate(screen.route) {
// Pop up to the start destination of the graph to
// avoid building up a large stack of destinations
// on the back stack as users select items
popUpTo(navController.graph.findStartDestination().id) {
saveState = true
}
// Avoid multiple copies of the same destination when
// reselecting the same item
launchSingleTop = true
// Restore state when reselecting a previously selected item
restoreState = true
}
}
Save your state, save your users
One of the most frustrating things for a user is losing their state. That’s one of the reasons why fragments have a whole page on saving state and one of the many reasons why I am so glad to get each layer updated to support multiple back stacks:
Fragments (i.e., without using the Navigation Component at all): this is an opt-in change by using the new FragmentManager APIs of saveBackStack and restoreBackStack.
The core Navigation Runtime: adds opt-in new NavOptions methods for restoreState and saveState and a new overload of popBackStack() that also accepts a saveState boolean (defaults to false).
Navigation with Fragments: the FragmentNavigator now utilizes the new Navigator APIs to properly translate the Navigation Runtime APIs into the Fragment APIs by using the Navigation Runtime APIs.
NavigationUI: The onNavDestinationSelected(), NavigationBarView.setupWithNavController(), and NavigationView.setupWithNavController() now use the new restoreState and saveState NavOptions by default whenever they would pop the back stack. This means that every app using those NavigationUI APIs will get multiple back stacks without any code changes on their part after upgrading the Navigation 2.4.0-alpha01 or higher.

Navigation Component "Back" via device reloads Fragment

The two screens relevant to my issue amount to MainFragment (a master view) and ProfileFragment (a detail view). The launch path from login -> main -> profile works fine. The return path isn't working as well. I get stranded at main when navigating backward with via gesture or device button.
On the initial "back" from the profile, I'm returned to main and see main's onViewCreated, onResume lifecycles. I expect back from main would return to login or even exit the app, but instead it reloads main a dozen times before crashing.
For any subsequent "back" from main, the logs reveal main's onAttach, onCreate, onViewCreated, onResume lifecycles recurring each time. This seems suspicious although it's a pretty vanilla navigation graph, a single Activity hosting four basic Fragments. They're all navigated with basic actions using their generated, type-safe Directions classes. I haven't overridden onBackPressed in any of these classes.
Combinations of app:popUpTo="#+id/loginFragment" and/or app:popUpToInclusive="true" haven't helped.
Do I need to implement back or "up" in order to work as expected? Any thoughts are appreciated.
On closer inspection, it's working the way it's coded. Of course it is!
The preceding LoginFragment checks a SharedPreference value and navigates to main. I confirmed that happens so quickly I thought I never returned to login screen, when in fact I did.
I'll investigate removing login screen from the stack after authentication.

Avoiding Android navigation IllegalArgumentException in NavController [duplicate]

This question already has answers here:
IllegalArgumentException: navigation destination xxx is unknown to this NavController
(40 answers)
Closed 2 years ago.
I recently switched over to Android Navigation, but have encountered a fair amount of situations (in different parts of the code), where I get:
Fatal Exception: java.lang.IllegalArgumentException
navigation destination com.xxx.yyy:id/action_aFragment_to_bFragment is unknown to this NavController
In every case, the code are simple calls like:
findNavController(this, R.id.navigation_host_fragment).navigate(R.id.action_aFragment_to_bFragment)
Usually in response to a button press.
It's unclear why this error is getting thrown. My current suspicion is that the onClickListener is somehow getting called twice on some devices, causing the navigate to be called a second time (causing it to be in the wrong state at the time). The reason for this suspicion is that this most often seems to happen in cases where one might have a "long" running operation before the navigate call. I can't recreate this on my own device, though.
Ideas on how to avoid this issue (or indeed, what the actual root cause of the issue is)?
I don't want to use Global Actions; I'm wary of introducing even more unexpected states to the backstack. And I'd really prefer not to have to try and test for the currentstate every time a navigate call is made.
You can use the below code before navigating to check whether the current destination is correct or not. This will make sure that the call happens from the current fragment only. This issue can be reproduced by clicking on two views simultaneously(Two items in a list).
if (findNavController().currentDestination?.id == R.id.currentFragment) {
findNavController().navigate(R.id.action_current_next)
}
Okay, let me explain you this exception was pass because we are calling an action from a fragment(destination) which is not the current destination on the stack.
i.e
you're calling an action
R.id.action_aFragment_to_bFragment
from fragmentA but in navController current destination is other that fragmentA. That's why navController through that exception:
navigation destination com.xxx.yyy:id/action_aFragment_to_bFragment is unknown to this NavController
you can check the current destination before navigate. like
Toast.makeText(context,view?.findNavController()?.currentDestination?.label,Toast.LENGTH_SHORT).show()
That will show you the current destination and i'm sure that's will be some other destination.
This will happens when we replace a fragment other than actions(like via old methods not with navigation) or we popUp that fragment before calling an action.
If that will be the case then you have to use Global Action because they don't care what current destination is.

How to avoid multiple instances of a fragment in back stack?

I want to avoid that in this navigation use case: A -> B -> A -> B -> A -> B ...
Where all the fragment instances are kept in the back stack. Reason: Avoid out of memory error.
I tried creating an own navigation workflow, like described here: https://stackoverflow.com/questions/18041583/fragments-backstack-issue?noredirect=1#comment26393904_18041583 (which is supposed to mimic activity behaviour calling always finish() after starting a new one, together with letting only the very first one (home) in the navigation stack).
But it seems to be either very wrong or ununderstandable.
So I thought, also, to implement a behaviour like activity "bring to front" flag. But I don't know how to do it. Maybe something with popBackStack - but I don't know how to ask the fragment if the transaction already is in the backstack. And I don't know if I'm on the right path.
This should be a quite standard task, since every navigation menu basically has this problem. But still, seems not to be straight forward to implement, and also can't find information about it.
Any idea?
Take a look at the FragmentManager backstack. It has facilities for looking at/popping entries in the fragment backstack. You might want logic something along the lines of: if the user is asking for a fragment that is at the top of the stack (the previous fragment) then exit this fragment (go back) otherwise start a new one.
That would produce:
A (user asks for B)
A->B (user asks for A again)
A
.. but would not prevent
A (user asks for B)
A->B (user asks for C)
A->B->C (user asks for A)
A->B->C->A
That would require rewinding the stack back to "A" from "C", which you can do.. but then if that is the case, perhaps you should be unconditionally popping the fragment stack before starting a new fragment (I.E. No back stack at all..)

Categories

Resources