I am having issue with the new Android Navigation Architecture component when I try to navigate from one Fragment to another, I get this weird error:
TL;DR 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:
navigate
invocation always works fine;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
.
You can check requested action in current destination of navigation controller.
UPDATE added usage of global actions for safe navigation.
fun NavController.navigateSafe(
@IdRes resId: Int,
args: Bundle? = null,
navOptions: NavOptions? = null,
navExtras: Navigator.Extras? = null
) {
val action = currentDestination?.getAction(resId) ?: graph.getAction(resId)
if (action != null && currentDestination?.id != action.destinationId) {
navigate(resId, args, navOptions, navExtras)
}
}
In my case, if the user clicks the same view twice very very quickly, this crash will occur. So you need to implement some sort of logic to prevent multiple quick clicks... Which is very annoying, but it appears to be necessary.
You can read up more on preventing this here: Android Preventing Double Click On A Button
Edit 3/19/2019: Just to clarify a bit further, this crash is not exclusively reproducible by just "clicking the same view twice very very quickly". Alternatively, you can just use two fingers and click two (or more) views at the same time, where each view has their own navigation that they would perform. This is especially easy to do when you have a list of items. The above info on multiple click prevention will handle this case.
Edit 4/16/2020: Just in case you're not terribly interested in reading through that Stack Overflow post above, I'm including my own (Kotlin) solution that I've been using for a long time now.
class OnSingleClickListener : View.OnClickListener {
private val onClickListener: View.OnClickListener
constructor(listener: View.OnClickListener) {
onClickListener = listener
}
constructor(listener: (View) -> Unit) {
onClickListener = View.OnClickListener { listener.invoke(it) }
}
override fun onClick(v: View) {
val currentTimeMillis = System.currentTimeMillis()
if (currentTimeMillis >= previousClickTimeMillis + DELAY_MILLIS) {
previousClickTimeMillis = currentTimeMillis
onClickListener.onClick(v)
}
}
companion object {
// Tweak this value as you see fit. In my personal testing this
// seems to be good, but you may want to try on some different
// devices and make sure you can't produce any crashes.
private const val DELAY_MILLIS = 200L
private var previousClickTimeMillis = 0L
}
}
fun View.setOnSingleClickListener(l: View.OnClickListener) {
setOnClickListener(OnSingleClickListener(l))
}
fun View.setOnSingleClickListener(l: (View) -> Unit) {
setOnClickListener(OnSingleClickListener(l))
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
settingsButton.setOnSingleClickListener {
// navigation call here
}
}
If you click on too quickly , it will cause null and crash.
We can use RxBinding lib to help on this. You can add throttle and duration on the click before it happens.
RxView.clicks(view).throttleFirst(duration, TimeUnit.MILLISECONDS)
.subscribe(__ -> {
});
These articles on throttling on Android might help. Cheers!
As mentioned in other answers, this exception generally occurs when a user
Using a timer to disable clicks is not an appropriate way to handle this issue. If the user has not been navigated to the destination after the timer expires the app will crash anyways and in many cases where navigation is not the action to be performed quick clicks are necessary.
In case 1, android:splitMotionEvents="false"
in xml or setMotionEventSplittingEnabled(false)
in source file should help. Setting this attribute to false will allow only one view to take the click. You can read about it here
In case 2, there would be something delaying the navigation process allowing the user to click on a view multiple times(API calls, animations, etc). The root issue should be resolved if possible so that navigation happens instantaneously, not allowing the user to click the view twice. If the delay is inevitable, like in the case of an API call, disabling the view or making it unclickable would be the appropriate solution.
Today
def navigationVersion = "2.2.1"
The issue still exists. My approach on Kotlin is:
// To avoid "java.lang.IllegalArgumentException: navigation destination is unknown to this NavController", se more https://stackoverflow.com/q/51060762/6352712
fun NavController.navigateSafe(
@IdRes destinationId: Int,
navDirection: NavDirections,
callBeforeNavigate: () -> Unit
) {
if (currentDestination?.id == destinationId) {
callBeforeNavigate()
navigate(navDirection)
}
}
fun NavController.navigateSafe(@IdRes destinationId: Int, navDirection: NavDirections) {
if (currentDestination?.id == destinationId) {
navigate(navDirection)
}
}