TL;DR Navigation controller changes faster than UI and you send two identical navigate(R.id.destn_id)
to the navigation controller in two different states.
To fix wrap your navigate
calls with try-catch
(simple way), or make sure there will be only one call of navigate
in short period of time. This issue likely won't go away. Copy bigger code snippet in your app and try out.
Hello. Based on a couple of useful responses above, I would like to share my solution that can be extended.
Here is the code that caused this crash in my application:
@Override
public void onListItemClicked(ListItem item) {
Bundle bundle = new Bundle();
bundle.putParcelable(SomeFragment.LIST_KEY, item);
Navigation.findNavController(recyclerView).navigate(R.id.action_listFragment_to_listItemInfoFragment, bundle);
}
A way to easily reproduce the bug is to tap with multiple fingers on the list of items where click on each item resolves in the navigation to the new screen (basically the same as people noted - two or more clicks in a very short period of time). I noticed that:
- First
navigate
invocation always works fine;
- Second and all other invocations of the
navigate
method resolve in IllegalArgumentException
.
From my point of view, this situation may appear very often. Since the repeating of code is a bad practice and it is always good to have one point of influence I thought of the next solution:
public class NavigationHandler {
public static void navigate(View view, @IdRes int destination) {
navigate(view, destination, /* args */null);
}
/**
* Performs a navigation to given destination using {@link androidx.navigation.NavController}
* found via {@param view}. Catches {@link IllegalArgumentException} that may occur due to
* multiple invocations of {@link androidx.navigation.NavController#navigate} in short period of time.
* The navigation must work as intended.
*
* @param view the view to search from
* @param destination destination id
* @param args arguments to pass to the destination
*/
public static void navigate(View view, @IdRes int destination, @Nullable Bundle args) {
try {
Navigation.findNavController(view).navigate(destination, args);
} catch (IllegalArgumentException e) {
Log.e(NavigationHandler.class.getSimpleName(), "Multiple navigation attempts handled.");
}
}
}
And thus the code above changes only in one line from this:
Navigation.findNavController(recyclerView).navigate(R.id.action_listFragment_to_listItemInfoFragment, bundle);
to this:
NavigationHandler.navigate(recyclerView, R.id.action_listFragment_to_listItemInfoFragment, bundle);
It even became a little bit shorter. The code was tested in the exact place where the crash occurred. Did not experience it anymore, and will use the same solution for other navigations to avoid the same mistake further.
Any thoughts are welcome!
What exactly causes the crash
Remember that here we work with the same navigation graph, navigation controller and back-stack when we use method Navigation.findNavController
.
We always get the same controller and graph here. When navigate(R.id.my_next_destination)
is called graph and back-stack changes almost instantly while UI is not updated yet. Just not fast enough, but that is ok. After back-stack has changed the navigation system receives the second navigate(R.id.my_next_destination)
call. Since back-stack has changed we now operate relative to the top fragment in the stack. The top fragment is the fragment you navigate to by using R.id.my_next_destination
, but it does not contain next any further destinations with ID R.id.my_next_destination
. Thus you get IllegalArgumentException
because of the ID that the fragment knows nothing about.
This exact error can be found in NavController.java
method findDestination
.