Navigation Components: A fix for “Navigation action cannot be found in the current destination” crash.
If you use navigation components extensively to swap out and in your fragments, as Google recommended, you probably have encountered this error more than a few times:
Of course, there are times this error occurs(mostly during development phases) because you actually referenced a wrong action, in this case, it’s a simple solution: use the right action. My focus here is about those other times where you did everything right and it works but for some reason, you sometimes experience a crash or see logs on Crashlytics or other crash-reporting tool caused by this issue. This becomes even more interesting when you cannot even replicate this from your end.
The following parts of this article will be divided into 3 sections:
- Cause: I will explain why this error occurs
- Replication: I will explain how to replicate this issue
- Solution/Prevention: How to solve and subsequently prevent this from happening.
Section 1: Cause
This issue is caused when an event that should call the navigation action fires multiple times in quick succession usually as a result of these:
- A non-user triggered callback, e.g network response callback, sensor, view events and internet connectivity callback, that calls the navigation action is invoked more than once.
- A user-triggered action, e.g button clicks and other user-triggered view events, that houses the navigation action is invoked more than once.
It’s also important to note that the probability of this occurring increases if your app is slow or laggy. For instance, I had this issue a lot when I worked on an app with an infinite-scrolling Recyclerview housed within a Scrollview; this effectively turned off view recycling in the RecyclerView, in turn, making the screen laggy as more items were fetched and added. More on this here.
Section 2: Replication
To replicate this, I created a sample project, screenshot is shown below:
Clicking the “GO TO SCREEN 2” button calls performDoubleClick() which starts a For loop that triggers the navigation action twice. This is an attempt to recreate what happens when the screen lags allowing a user to tap on a button/view more than once.
The gif showing this crash replicated is shown below. The crash log is the header image above.
The crash occurs because the first invocation of findController.navigate() takes the app to a different screen/destination, on the second invocation, the navigation action passed to findController.navigate() is no longer valid because it cannot be reached from the new screen/destination.
Section 3: Solution/Prevention
Now that we have succeeded in replicating this issue, it is time to find a fix. If you followed along so far, you probably know what needs to be done:
ensure the navigation action isn't called multiple times.
While there are several possible ways to achieve this, these are the two I know and use:
- Using a Blocking ClickListener: This is basically an extension of the View OnClickListener to absorb multiple clicks within a specified waitTime. That is, no matter the number of clicks it receives within this waitTime, it will emit only one click event. See Gist below:
The function to look at is View.blockingClickListener, it allows setting custom wait times within which only one click event will be propagated and the rest ignored. To test this, I went the elaborate route. I added a new button named btn_double_click, clicking this button calls performDoubleClick() which programmatically clicks btn_open_screen_two twice. Note that btn_open_screen_two is using View.blockingClickListener and not the regular View.onClickListener. I added logs to show the behaviour. The result is shown below:
It is important to note that this method only works for user-triggered events.
2. Using a safeNavigate extension method: I learnt this from a Twitter post by Chris Banes(I think). This deals with the problem directly by making sure that navigation actions are not triggered if it does not exist in the current destination.
I came up with the second extension method that allows you to work with actionIds. Here, you pass the navigationId of the fragment where the navigation is being initiated as the currentDestinationId. That being said, you should stick to using NavDirections and safeArgs to benefit from its compile-time validations especially when action names and navigation arguments change. See the modified code below:
Logs from the test are shown below. As you can see, only one click is propagated.
This second method solves the issue completely for both user-triggered and non-user-triggered events so I recommend it although you should also prefer blocking click listeners to regular click listeners so click events are fired only once.
I hope you found this helpful. As always, feel free to drop questions, suggestions and comments.