224

I am having an issue with the new Android Navigation Architecture component when I try to navigate from one Fragment to another, I get this weird error:

java.lang.IllegalArgumentException: navigation destination XXX
is unknown to this NavController

Every other navigation works fine except this particular one.

I use findNavController() function of Fragment to get access to the NavController.

Any help will be appreciated.

Rahul
  • 3,293
  • 2
  • 31
  • 43
Jerry Okafor
  • 3,710
  • 3
  • 17
  • 26

40 Answers40

125

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.

OnSingleClickListener.kt

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
    }

}

ViewExt.kt

fun View.setOnSingleClickListener(l: View.OnClickListener) {
    setOnClickListener(OnSingleClickListener(l))
}

fun View.setOnSingleClickListener(l: (View) -> Unit) {
    setOnClickListener(OnSingleClickListener(l))
}

HomeFragment.kt

override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
    super.onViewCreated(view, savedInstanceState)

    settingsButton.setOnSingleClickListener {
        // navigation call here
    }
}
Charles Madere
  • 6,642
  • 5
  • 35
  • 34
  • 35
    The edit about using 2 fingers and clicking 2 views at the same time! That's the key for me and helped me to replicate the issue easily. Great updating with that information. – Richard Le Mesurier Apr 11 '19 at 16:19
  • During the debug phase I happened to click while the app was stuck waiting for continue the execution. Seems like another case of two subsequent clicks in a row to the IDE – marcolav Aug 22 '19 at 20:54
  • 1
    Thanks for this. Saved me a few crashes and some head scratching :) – ibyte Apr 27 '20 at 12:09
  • 9
    This solution is hack to get around the real problem: the navigation component. It's also prone to fail on slower devices. Creating and inflating a new fragment can definitively take over 200ms. After that delay, a second click event could be sent before the fragment is shown and we're back to the same problem. – Nicolas Aug 16 '20 at 21:15
  • Doesn't really work in my app. I still can click fast enough to produce the crash at least with a debug build. Looks like a problem with thread safety although there should actually only be one ui thread. Strange. – The incredible Jan Oct 28 '20 at 08:45
108

Check currentDestination before calling navigate might be helpful.

For example, if you have two fragment destinations on the navigation graph fragmentA and fragmentB, and there is only one action from fragmentA to fragmentB. calling navigate(R.id.action_fragmentA_to_fragmentB) will result in IllegalArgumentException when you were already on fragmentB. Therefor you should always check the currentDestination before navigating.

if (navController.currentDestination?.id == R.id.fragmentA) {
    navController.navigate(R.id.action_fragmentA_to_fragmentB)
}
Jian
  • 3,118
  • 2
  • 22
  • 36
  • 3
    I have a search app that navigates with an action with arguments. Thus it could navigate from the currentDestination to itself. I ended up doing the same except navController.currentDestination == navController.graph.node . It felt kinda dirty though and I feel like I shouldn't have to do this. – Shawn Maybush Jan 03 '19 at 09:22
  • 166
    The library shouldn't force us to make this check, it is indeed ridiculous. – DaniloDeQueiroz Apr 05 '19 at 19:07
  • I had the same issue. I had an EditText, and a 'save' button to store the content of the EditText in the database. It always crashed on pressing the 'save' button. I suspect that the reason has to do with the fact that, in order to be able to press the 'save' button, I need to get rid of the on-screen keyboard by tapping the back button. – The Fox Oct 17 '19 at 10:34
  • This checks for an error condition, but it does not solve the issue. Interestingly, this condition is true if the navigation backstack becomes empty for unwanted reasons. – Mike76 Nov 25 '19 at 16:43
  • How do you pass arguments?? – IgorGanapolsky May 27 '20 at 21:10
  • 2
    even in iOS though sometimes multiple ViewController gets pushed when you press the button multiple times. Guess both Android and iOS have this problem. – coolcool1994 Jun 12 '20 at 17:55
  • I was doing a navigation based on a live data value. Adding this fixed my problem. – CanonicalBear Nov 17 '20 at 05:56
  • **Q:** How do you get the correct fragment id? **A:** It is available in the nav_graph.xml, in the fragment tag: `` – Ben Butterworth Mar 04 '21 at 11:25
  • how i can use this solution from a fragment inside a view-pager as i don't have the destination id ? – Omar Beshary Mar 15 '21 at 15:24
65

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)
    }
}
Alex Nuts
  • 977
  • 8
  • 11
  • 1
    This solution won't work for any actions defined outside of the `currentDestination`'s action list. Say you have a global action defined and use that action to navigate. This will fail because the action isn't defined in the currentDestination's list. Adding a check like `currentDestination?.getAction(resId) != null || currentDestination?.id != resId` should resolve it but also might not cover every case. – wchristiansen Oct 02 '19 at 19:22
  • @wchristiansen, thank you for notes. I've updated code with usage of global actions – Alex Nuts Oct 15 '19 at 09:33
  • 1
    @AlexNuts great answer. I think you can remove `?: graph.getAction(resId)` -> `currentDestination?.getAction(resId)` will return an action for both Global or non-Global actions (I've tested it). Also, would be better if you made use of Safe Args -> rather pass in `navDirections: NavDirections` than `resId` and `args` separately. – Wess Mar 03 '20 at 11:41
  • 1
    @AlexNuts Note this solution does not support navigating to the same destination as the current destination. I.o.w. navigating from destination X with Bundle Y to destination X with Bundle Z is not possible. – Wess Mar 03 '20 at 11:52
  • This will fail if the action navigates to a nested graph. In this case, the action destination ID will be that of the nested graph and not that of its start destination, bypassing the check. – Nicolas Aug 16 '20 at 21:18
  • 1
    Thanks for the idea! But from my point of view, it's much easier to just handle exceptions in navigateSafe wrapper. I ended up with the following solution: https://vadzimv.dev/2021/07/26/android-jetpack-navigation-navigate-safe.html – VadzimV Jul 27 '21 at 14:43
53

What I did to prevent the crash is the following:

I have a BaseFragment, in there I've added this fun to ensure that the destination is known by the currentDestination:

fun navigate(destination: NavDirections) = with(findNavController()) {
    currentDestination?.getAction(destination.actionId)
        ?.let { navigate(destination) }
}

Worth noting that I'm using the SafeArgs plugin.

Douglas Kazumi
  • 1,206
  • 10
  • 14
21

It could also happen if you have a Fragment A with a ViewPager of Fragments B And you try to navigate from B to C

Since in the ViewPager the fragments are not a destination of A, your graph wouldn't know you are on B.

A solution can be to use ADirections in B to navigate to C

AntPachon
  • 1,152
  • 12
  • 14
  • In this case, the crash doesn't happen every time but only happens rare. How to solve it? – Srikar Reddy May 14 '19 at 14:07
  • You can add a global action inside the navGraph and use it to navigate – Abraham Mathew Jul 05 '19 at 10:59
  • 1
    As B shouldn't need to be aware of its exact parent, it would be better to use ADirections via an interface like `(parentFragment as? XActionListener)?.Xaction()` and note you could hold this function as a local variable if that is helpful – hmac Sep 17 '19 at 07:50
  • can you please share a sample code to illustrate this as I have same issue – Ikhiloya Imokhai Dec 11 '19 at 13:41
  • anybody could plz a sample code, i am stuck at the same issue. Have a fragment and then a tabfragment – Usman Zafer Mar 24 '20 at 17:25
17

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:

  1. First navigate invocation always works fine;
  2. 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.

Jenea Vranceanu
  • 4,530
  • 2
  • 18
  • 34
  • You can reproduce this issue? If so, please consider making a sample and write it here: https://issuetracker.google.com/issues/273600320 – android developer Mar 15 '23 at 00:06
  • Haven't tried for quite a long time. But I suppose this is still an issue. You can try force calling the `navigate` function twice and see for yourself if this is still an issue. Just write it one line after the other. – Jenea Vranceanu Mar 16 '23 at 21:31
  • I tried now to call "navigate" twice and it caused a crash, but according to Google this is logical because it's done right away, so it doesn't let me to go from: https://issuetracker.google.com/issues/274016275#comment2 , so this I understand, but still I want to be able to avoid such issues when it's not quite in my control. What I'm interested in is about other cases, that for some reason won't let me navigate . I was told that if I'm on onResume, it should be safe, but it's actually incorrect: issuetracker.google.com/issues/273978797 – android developer Mar 17 '23 at 19:54
  • I've read the posts on the issuetracker you've linked. TBH, I do not think that with the current implementation of navigation library there is a "safe" place to call `navigate` function from. This is a perfect example of using global state (NavigationController is handled somewhere behind the scene) and side-effects caused by calling `navigate`. I genuinely believe that this won't be fixed unless it will be given a long deep thorough thought on the side of Android dev team. – Jenea Vranceanu Mar 18 '23 at 21:31
  • I imagine that there could be a solution they could provide. For example (just off the top of my head): when calling `findNavController` it should return a navigation controller that has some specific information representing the state of the current fragment or activity you call that function in. Thus, as you discussed on issue tracker, it will have sufficient information for a developer to decide if a `navigation` function should be called. – Jenea Vranceanu Mar 18 '23 at 21:35
  • Or that feature could be even implemented within the navigation controller itself: if a fragment is paused (not at the top of a stack) - do not allow navigation event to happen. Again, this is just an idea, but it's up to developers at Google to decide. I've left a comment on issue tracker. Cannot really devote too much time to this issue now but will monitor it when I have some spare time and if I'll receive notifications. – Jenea Vranceanu Mar 18 '23 at 21:56
  • I'm not sure but maybe this could tell you if you can reach the new destination: `newDestinationId=currentBackStackEntry.destination.getAction(navigationActionId)?.destinationId` . If this is null, you can't use the navigation action ID from the current location . In normal cases, you could use `newDestination = newDestinationId?.let { currentNode.parent?.findNode(newDestinationId) } ` on this. I don't know if this is always helping, but they should have offered us some functions to help us about navigation, and make it much easier and shorter to use, too. – android developer Mar 19 '23 at 00:38
  • Checking for `currentBackStackEntry.destination.getAction(navigationActionId)?.destinationId != null` looks like a solution. You can write an extension function in Kotlin for Fragment and Activity and use that check in there. I understand their intention - crash to highlight if something went wrong, - but it just doesn't fit well enough in default circumstances. – Jenea Vranceanu Mar 19 '23 at 08:39
  • I think it will work in most cases, but not in others. For example, navigating from current Fragment to a new instance of it. I know that's rare, but it will probably fail in this case. Maybe in other special cases too – android developer Mar 20 '23 at 07:14
15

Try that

  1. Create this extension function (or normal function):

UPDATED (without reflection and more readable)

import androidx.fragment.app.Fragment
import androidx.navigation.NavController
import androidx.navigation.NavDirections
import androidx.navigation.fragment.DialogFragmentNavigator
import androidx.navigation.fragment.FragmentNavigator

fun Fragment.safeNavigateFromNavController(directions: NavDirections) {
    val navController = findNavController()
    when (val destination = navController.currentDestination) {
        is FragmentNavigator.Destination -> {
            if (javaClass.name == destination.className) {
                navController.navigate(directions)
            }
        }
        is DialogFragmentNavigator.Destination -> {
            if (javaClass.name == destination.className) {
                navController.navigate(directions)
            }
        }
    }
}

OLD (with reflection)

import androidx.fragment.app.Fragment
import androidx.navigation.NavController
import androidx.navigation.NavDirections
import androidx.navigation.fragment.FragmentNavigator

inline fun <reified T : Fragment> NavController.safeNavigate(directions: NavDirections) {
    val destination = this.currentDestination as FragmentNavigator.Destination
    if (T::class.java.name == destination.className) {
        navigate(directions)
    }
}
  1. And use like this from your Fragment:
val direction = FragmentOneDirections.actionFragmentOneToFragmentTwo()
// new usage
safeNavigateFromNavController(direction)

// old usage
// findNavController().safeNavigate<FragmentOne>(action)

My problem was

I have a fragment (FragmentOne) that goes to two others fragments (FragmentTwo and FragmentThree). In some low devices, the user press button that redirects to FragmentTwo but in few milliseconds after the user press button that redirects to FragmentThree. The results is:

Fatal Exception: java.lang.IllegalArgumentException Navigation action/destination action_fragmentOne_to_fragmentTwo cannot be found from the current destination Destination(fragmentThree) class=FragmentThree

My solve was:

I check if the current destination belongs to the current fragment. If true, I execute the navigation acion.

That is all!

Arpan Sarkar
  • 2,301
  • 2
  • 13
  • 24
Abner Escócio
  • 2,697
  • 2
  • 17
  • 36
14

In my case I was using a custom back button for navigating up. I called onBackPressed() in stead of the following code

findNavController(R.id.navigation_host_fragment).navigateUp()

This caused the IllegalArgumentException to occur. After I changed it to use the navigateUp() method in stead, I didn't have a crash again.

the-ginger-geek
  • 7,041
  • 4
  • 27
  • 45
  • I don't get what the difference is between onBackPressed and this, still stuck with the system back button and overriding it and replacing with this seems crazy – Daniel Wilson Feb 04 '19 at 13:22
  • 2
    I agree it does seem crazy. Many of the things I've encountered in the android navigation architecture component feels a bit crazy, it's set up too rigidly IMO. Thinking about doing my own implementation for our project as it's just creating too many headaches – the-ginger-geek Feb 05 '19 at 09:31
  • Does not work for me... Still getting the same error. – Otziii May 07 '19 at 13:28
7

In my case, the issue occurred when I had re-used one of my Fragments inside a viewpager fragment as a child of the viewpager. The viewpager Fragment(which was the parent fragment) was added in the Navigation xml, but the action was not added in the viewpager parent fragment.

nav.xml
//reused fragment
<fragment
    android:id="@+id/navigation_to"
    android:name="com.package.to_Fragment"
    android:label="To Frag"
    tools:layout="@layout/fragment_to" >
    //issue got fixed when i added this action to the viewpager parent also
    <action android:id="@+id/action_to_to_viewall"
        app:destination="@+id/toViewAll"/>
</fragment>
....
// viewpager parent fragment
<fragment
    android:id="@+id/toViewAll"
    android:name="com.package.ViewAllFragment"
    android:label="to_viewall_fragment"
    tools:layout="@layout/fragment_view_all">

Fixed the issue by adding the action to the parent viewpager fragment also as shown below:

nav.xml
//reused fragment
<fragment
    android:id="@+id/navigation_to"
    android:name="com.package.to_Fragment"
    android:label="To Frag"
    tools:layout="@layout/fragment_to" >
    //issue got fixed when i added this action to the viewpager parent also
    <action android:id="@+id/action_to_to_viewall"
        app:destination="@+id/toViewAll"/>
</fragment>
....
// viewpager parent fragment
<fragment
    android:id="@+id/toViewAll"
    android:name="com.package.ViewAllFragment"
    android:label="to_viewall_fragment"
    tools:layout="@layout/fragment_view_all"/>
    <action android:id="@+id/action_to_to_viewall"
        app:destination="@+id/toViewAll"/>
</fragment>
hushed_voice
  • 3,161
  • 3
  • 34
  • 66
6

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)
    }
}
Serg Burlaka
  • 2,351
  • 24
  • 35
5

In my case, I had multiple nav graph files and I was trying to move from 1 nav graph location to a destination in another nav graph.

For this we have to include the 2nd nav graph in the 1st one like this

<include app:graph="@navigation/included_graph" />

and add this to your action:

<action
        android:id="@+id/action_fragment_to_second_graph"
        app:destination="@id/second_graph" />

where second_graph is :

<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"
    android:id="@+id/second_graph"
    app:startDestination="@id/includedStart">

in the second graph.

More info here

hushed_voice
  • 3,161
  • 3
  • 34
  • 66
5

You can check before navigating if the Fragment requesting the navigation is still the current destination, taken from this gist.

It basically sets a tag on the fragment for later lookup.

/**
 * Returns true if the navigation controller is still pointing at 'this' fragment, or false if it already navigated away.
 */
fun Fragment.mayNavigate(): Boolean {

    val navController = findNavController()
    val destinationIdInNavController = navController.currentDestination?.id
    val destinationIdOfThisFragment = view?.getTag(R.id.tag_navigation_destination_id) ?: destinationIdInNavController

    // check that the navigation graph is still in 'this' fragment, if not then the app already navigated:
    if (destinationIdInNavController == destinationIdOfThisFragment) {
        view?.setTag(R.id.tag_navigation_destination_id, destinationIdOfThisFragment)
        return true
    } else {
        Log.d("FragmentExtensions", "May not navigate: current destination is not the current fragment.")
        return false
    }
}

R.id.tag_navigation_destination_id is just an id you'll have to add to your ids.xml, to make sure it's unique. <item name="tag_navigation_destination_id" type="id" />

More info on the bug and the solution, and navigateSafe(...) extention methods in "Fixing the dreaded “… is unknown to this NavController”

Frank
  • 12,010
  • 8
  • 61
  • 78
  • 1
    I've studied a few different solutions to this problem, and yours is definitely the nicest. Makes me sad to see so little love for it – Luke Needham May 07 '20 at 17:21
  • 1
    it may be useful to create a unique identifier in place of `NAV_DESTINATION_ID` with something like this https://stackoverflow.com/a/15021758/1572848 – William Reed May 08 '20 at 15:16
  • Where's the tag coming from and why is it needed? I've problems where the actual id's on the navigation component don't match these from `R.id`. – riezebosch Jun 25 '20 at 13:19
  • `R.id.tag_navigation_destination_id` is just an id you'll have to add to your ids.xml, to make sure it's unique. `` – Frank Jun 25 '20 at 13:58
  • Even with this solution, it is possible that the original crash still occurs when popping the backstack. You may wish to add `fun Fragment.popBackStackSafe() { if (mayNavigate()) findNavController().popBackStack() }` – Luke Needham Sep 21 '20 at 14:34
4

I wrote this extensions

fun Fragment.navigateAction(action: NavDirections) {
    val navController = this.findNavController()
    if (navController.currentDestination?.getAction(action.actionId) == null) {
        return
    } else {
        navController.navigate(action)
    }
}
4

I have resolved the same problem by putting check before navigate instead of boilerplate code for clicking instantly control

 if (findNavController().currentDestination?.id == R.id.currentFragment) {
        findNavController().navigate(R.id.action_current_next)}
/* Here R.id.currentFragment is the id of current fragment in navigation graph */

according to this answer

https://stackoverflow.com/a/56168225/7055259

Gk Mohammad Emon
  • 6,084
  • 3
  • 42
  • 42
mohammad
  • 91
  • 1
  • 4
4

As mentioned in other answers, this exception generally occurs when a user

  1. clicks on multiple views at the same time that handle navigation
  2. clicks multiple times on a view that handles navigation.

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.

Xid
  • 4,578
  • 2
  • 13
  • 35
3

In my case the bug ocurred because I had a navigation action with the Single Top and the Clear Task options enabled after a splash screen.

Eury Pérez Beltré
  • 2,017
  • 20
  • 28
3

It seems that mixing fragmentManager control of the backstack and Navigation Architecture control of the backstack can cause this issue also.

For example the original CameraX basic sample used fragmentManager backstack navigation as below and it appears as if it did not correctly interact with Navigation:

// Handle back button press
        view.findViewById<ImageButton>(R.id.back_button).setOnClickListener {
            fragmentManager?.popBackStack()
        }

If you log the 'current destination' with this version before moving from the main fragment (the camera fragment in this case) and then log it again when you return to the main fragment, you can see from the id in the logs that the id is not the same. At a guess, the Navigation updated it when moving to the fragment and the fragmntManager did not then update it again when moving back. From the logs:

Before: D/CameraXBasic: currentDest?: androidx.navigation.fragment.FragmentNavigator$Destination@b713195

After: D/CameraXBasic: currentDest?: androidx.navigation.fragment.FragmentNavigator$Destination@9807d8f

The updated version of CameraX basic sample uses Navigation to return like this:

 // Handle back button press
        view.findViewById<ImageButton>(R.id.back_button).setOnClickListener {
            Navigation.findNavController(requireActivity(), R.id.fragment_container).navigateUp()
        }

This works correctly and the logs show the same id when back at the main fragment.

Before: D/CameraXBasic: currentDest?: androidx.navigation.fragment.FragmentNavigator$Destination@b713195

After: D/CameraXBasic: currentDest?: androidx.navigation.fragment.FragmentNavigator$Destination@b713195

I suspect the moral of the story, at least at this time, is to be very careful mixing Navigation with fragmentManager navigation.

Community
  • 1
  • 1
Mick
  • 24,231
  • 1
  • 54
  • 120
  • This sounds plausible, I will investigate further. Has anyone been able to verify or substantiate this claim? – Jerry Okafor May 06 '20 at 11:23
  • @JerryOkafor - I have tested it in an app I was working on based on CameraX Sample and verified it but it would be good to see if someone else has seen this also. I actually missed a 'back navigation' in one place in the same app so just fixed it again recently also. – Mick May 06 '20 at 11:33
  • I can substantiate the "moral of the story" if not the exact code. Just took on a project that uses `FragmentManager` to (incorrectly) get the "current fragment" at which time it "safely navigates". This answer resonates with me because I noted the 2 fragments in question are often not the same (and hence the supposed safe navigation code still crashes the app). Thx Mick for the first suggestion that I was possibly on the right track after all. – Richard Le Mesurier Mar 15 '22 at 15:37
3

A Ridiculous way but very powerful is: Simply call this:

view?.findNavController()?.navigateSafe(action)

Just Create this Extention:

fun NavController.navigateSafe(
    navDirections: NavDirections? = null
) {
    try {
        navDirections?.let {
            this.navigate(navDirections)
        }
    }
    catch (e:Exception)
    {
        e.printStackTrace()
    }
}
Amir Hossein Ghasemi
  • 20,623
  • 10
  • 57
  • 53
  • Thank you for this simple to understand yet powerful example. No need to mess around with click listeners and we can still use all of the APIs =D – baskInEminence Sep 12 '20 at 06:19
3

After thinking over Ian Lake's advice in this twitter thread I've came up with following approach. Having NavControllerWrapper defined as such:

class NavControllerWrapper constructor(
  private val navController: NavController
) {

  fun navigate(
    @IdRes from: Int,
    @IdRes to: Int
  ) = navigate(
    from = from,
    to = to,
    bundle = null
  )

  fun navigate(
    @IdRes from: Int,
    @IdRes to: Int,
    bundle: Bundle?
  ) = navigate(
    from = from,
    to = to,
    bundle = bundle,
    navOptions = null,
    navigatorExtras = null
  )

  fun navigate(
    @IdRes from: Int,
    @IdRes to: Int,
    bundle: Bundle?,
    navOptions: NavOptions?,
    navigatorExtras: Navigator.Extras?
  ) {
    if (navController.currentDestination?.id == from) {
      navController.navigate(
        to,
        bundle,
        navOptions,
        navigatorExtras
      )
    }
  }

  fun navigate(
    @IdRes from: Int,
    directions: NavDirections
  ) {
    if (navController.currentDestination?.id == from) {
      navController.navigate(directions)
    }
  }

  fun navigateUp() = navController.navigateUp()

  fun popBackStack() = navController.popBackStack()
}

Then in navigation code:

val navController = navControllerProvider.getNavController()
navController.navigate(from = R.id.main, to = R.id.action_to_detail)

azizbekian
  • 60,783
  • 13
  • 169
  • 249
  • Kotlin has extension functions exactly for that purpose, no need for a wrapper. – Nicolas Aug 16 '20 at 21:22
  • You cannot perform unit testing when you are using extension functions here and there. Alternatively, when you are using wrapper, you are introducing a seam in the component hence you are able to mock out components per your needs and perform pure unit testing. – azizbekian Aug 17 '20 at 08:24
3

I resolve this issue by checking if the next action exist in the current destination

public static void launchFragment(BaseFragment fragment, int action) {
    if (fragment != null && NavHostFragment.findNavController(fragment).getCurrentDestination().getAction(action) != null) {       
        NavHostFragment.findNavController(fragment).navigate(action);
    }
}

public static void launchFragment(BaseFragment fragment, NavDirections directions) {
    if (fragment != null && NavHostFragment.findNavController(fragment).getCurrentDestination().getAction(directions.getActionId()) != null) {       
        NavHostFragment.findNavController(fragment).navigate(directions);
    }
}

This resolve a problem if the user click fast on 2 differents button

Vodet
  • 1,491
  • 1
  • 18
  • 36
3

Update to @AlexNuts answer to support navigating to a nested graph. When an action uses a nested graph as a destination like so:

<action
    android:id="@+id/action_foo"
    android:destination="@id/nested_graph"/>

The destination ID of this action cannot be compared with the current destination because the current destination cannot be a graph. The start destination of the nested graph must be resolved.

fun NavController.navigateSafe(directions: NavDirections) {
    // Get action by ID. If action doesn't exist on current node, return.
    val action = (currentDestination ?: graph).getAction(directions.actionId) ?: return
    var destId = action.destinationId
    val dest = graph.findNode(destId)
    if (dest is NavGraph) {
        // Action destination is a nested graph, which isn't a real destination.
        // The real destination is the start destination of that graph so resolve it.
        destId = dest.startDestination
    }
    if (currentDestination?.id != destId) {
        navigate(directions)
    }
}

However this will prevent navigating to the same destination twice, which is sometimes needed. To allow that, you can add a check to action.navOptions?.shouldLaunchSingleTop() and add app:launchSingleTop="true" to the actions for which you don't want duplicated destinations.

Nicolas
  • 6,611
  • 3
  • 29
  • 73
3

In order to avoid this crash one of my colleagues wrote a small library which exposes a SafeNavController, a wrapper around the NavController and handles the cases when this crash occurs because of multiple navigate commands at the same time.

Here is a short article about the whole issue and the solution.

You can find the library here.

Dharman
  • 30,962
  • 25
  • 85
  • 135
Nagy Arthur
  • 51
  • 1
  • 3
3

Yet another solution to the same quick-click-navigation-crash problem:

fun NavController.doIfCurrentDestination(@IdRes destination: Int, action: NavController.()-> Unit){
    if(this.currentDestination?.id == destination){action()}
}

and then use like this:

findNavController().doIfCurrentDestination(R.id.my_destination){ navigate(...) }

benefits of this solutions is that you can easily wrap any existing call to naviagte() with whatever signature you already use, no need to make a million overloads

Mardann
  • 1,953
  • 1
  • 16
  • 23
2

I got this same error because I used a Navigation Drawer and getSupportFragmentManager().beginTransaction().replace( ) at the same time somewhere in my code .

I got rid of the error by using this condition(testing if the destination) :

if (Navigation.findNavController(v).getCurrentDestination().getId() == R.id.your_destination_fragment_id)
Navigation.findNavController(v).navigate(R.id.your_action);

In my case the previous error was triggered when I was clicking on the navigation drawer options. Basically the code above did hide the error , because in my code somewhere I used navigation using getSupportFragmentManager().beginTransaction().replace( ) The condition -

 if (Navigation.findNavController(v).getCurrentDestination().getId() ==
  R.id.your_destination_fragment_id) 

was never reached because (Navigation.findNavController(v).getCurrentDestination().getId() was always poiting to home fragment. You must only use Navigation.findNavController(v).navigate(R.id.your_action) or nav graph controller functions for all your navigation actions.

Gk Mohammad Emon
  • 6,084
  • 3
  • 42
  • 42
Aness
  • 610
  • 1
  • 7
  • 24
2

There could be many reasons for this problem. In my case i was using the MVVM model and i was observing a boolean for navigation when the boolean is true -> navigate else don't do anything and this was working fine but there was one mistake here

when pressing back button from the destination fragment i was encountering the same problem .And problem was the boolean object as I forgot to change the boolean value to false this created the mess.I just created a function in viewModel to change its value to false and called it just after the findNavController()

2

Throwing my answer into the ring that handles the two cases (double tap, simultaneous tap on two buttons) elegantly yet tries to not mask real errors.

We can use a navigateSafe() function that checks to see if the destination that we're trying to navigate to is invalid from the current destination but is valid from the previous destination. If this is the case, the code assumes the user either double tapped or tapped two buttons simultaneous.

This solution isn't perfect however as it may mask real problems in niche cases where we try to navigate to something that just so happens to be a destination of the parent. It is assumed though that this would be unlikely.

Code:

fun NavController.navigateSafe(directions: NavDirections) {
    val navigateWillError = currentDestination?.getAction(directions.actionId) == null

    if (navigateWillError) {
        if (previousBackStackEntry?.destination?.getAction(directions.actionId) != null) {
            // This is probably some user tapping two different buttons or one button twice quickly
            // Ignore...
            return
        }

        // This seems like a programming error. Proceed and let navigate throw.
    }

    navigate(directions)
}
idunnololz
  • 8,058
  • 5
  • 30
  • 46
1

I caught this exception after some renames of classes. For example: I had classes called FragmentA with @+is/fragment_a in navigation graph and FragmentB with @+id/fragment_b. Then I deleted FragmentA and renamed FragmentB to FragmentA. So after that node of FragmentA still stayed in navigation graph, and android:name of FragmentB's node was renamed path.to.FragmentA. I had two nodes with the same android:name and different android:id, and the action I needed were defined on node of removed class.

VasyaFromRussia
  • 1,872
  • 2
  • 14
  • 18
1

It occurs to me when I press the back button two times. At first, I intercept KeyListener and override KeyEvent.KEYCODE_BACK. I added the code below in the function named OnResume for the Fragment, and then this question/issue is solved.

  override fun onResume() {
        super.onResume()
        view?.isFocusableInTouchMode = true
        view?.requestFocus()
        view?.setOnKeyListener { v, keyCode, event ->
            if (event.action == KeyEvent.ACTION_DOWN && keyCode == KeyEvent.KEYCODE_BACK) {
                activity!!.finish()
                true
            }
            false
        }
    }

When it happens to me for a second time, and it's status is the same as the first one, I find that I maybe use the adsurd function. Let’s analyze these situations.

  1. Firstly, FragmentA navigates to FragmentB ,then FragmentB navigates to FragmentA, then press back button... the crash appears.

  2. Secondly, FragmentA navigates to FragmentB, then FragmentB navigates to FragmentC, FragmentC navigates to FragmentA, then press back button... the crash appears.

So I think when pressing back button, FragmentA will return to FragmentB or FragmentC, then it causes the login mess. Finally I find that the function named popBackStack can be used for back rather than navigate.

  NavHostFragment.findNavController(this@TeacherCloudResourcesFragment).
                        .popBackStack(
                            R.id.teacher_prepare_lesson_main_fragment,false
                        )

So far, the problem is really solved.

RobC
  • 22,977
  • 20
  • 73
  • 80
唐德坤
  • 61
  • 1
  • 1
1

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!

Sairam
  • 375
  • 1
  • 5
  • 28
Joshua
  • 61
  • 1
  • 9
1

I am calling the 2.3.1 Navigation and the same error occurs when the application configuration changes. When the cause of the problem was found through Debug, the GaphId in NavHostFragment did not take effect as the ID currently set by calling navController.setGraph(). The GraphId of NavHostFragment can only be obtained from the <androidx.fragment.app.FragmentContainerView/> tag. At this time, this problem will occur if there are multiple GraphIds dynamically set in your code. When the interface is restored, the Destination cannot be found in the cached GraphId. You can solve this problem by manually specifying the value of mGraphId in NavHostFragment through reflection when switching Graph.

navController.setGraph(R.navigation.home_book_navigation);
try {
    Field graphIdField = hostFragment.getClass().getDeclaredField("mGraphId");
    graphIdField.setAccessible(true);
    graphIdField.set(navHostFragment, R.navigation.home_book_navigation);
} catch (NoSuchFieldException | IllegalAccessException e) {
    e.printStackTrace();
}
MaxV
  • 2,601
  • 3
  • 18
  • 25
wangke
  • 11
  • 1
1

In my case, I resolved this issue by verifying all navigation actions are properly managed in to the respective graphs and updated code for device back press button like below in my main activity file:

 onBackPressedDispatcher.addCallback(this /* lifecycle owner */, object : OnBackPressedCallback(true) {
        override fun handleOnBackPressed() {
            // Back is pressed... Finishing the activity
            if (navHostFragment.childFragmentManager.backStackEntryCount == 0) {
                // First fragment is open, backstack is empty
               finish()
            } else {
                navHostFragment.navController.popBackStack()
            }
        }
    })
Arti Patel
  • 671
  • 4
  • 15
0

It seems like you are clearing task. An app might have a one-time setup or series of login screens. These conditional screens should not be considered the starting destination of your app.

https://developer.android.com/topic/libraries/architecture/navigation/navigation-conditional

Patrick
  • 1,629
  • 5
  • 23
  • 44
0

This happened to me, my issue was I was clicking a FAB on tab item fragment. I was trying to navigate from one of tab item fragment to another fragment.

But according to Ian Lake in this answer we have to use tablayout and viewpager, no navigation component support. Because of this, there is no navigation path from tablayout containing fragment to tab item fragment.

ex:

containing fragment -> tab layout fragment -> tab item fragment -> another fragment

Solution was to create a path from tab layout containing fragment to intended fragment ex: path: container fragment -> another fragment

Disadvantage:

  • Nav graph no longer represent user flow accurately.
user158
  • 12,852
  • 7
  • 62
  • 94
0

Updated @Alex Nuts solution

If there is no action for particular fragment and want to navigate to fragment

fun NavController.navigateSafe(
@IdRes actionId: Int, @IdRes fragmentId: Int, args: Bundle? = null,
navOptions: NavOptions? = null, navExtras: Navigator.Extras? = null) 
{
  if (actionId != 0) {
      val action = currentDestination?.getAction(actionId) ?: graph.getAction(actionId)
      if (action != null && currentDestination?.id != action.destinationId) {
          navigate(actionId, args, navOptions, navExtras)
    }
    } else if (fragmentId != 0 && fragmentId != currentDestination?.id)
        navigate(fragmentId, args, navOptions, navExtras)
}
Sumit
  • 1,022
  • 13
  • 19
0

Usually when this happens to me, I had the issue described by Charles Madere: Two navigation events triggered on the same ui, one changing the currentDestination and the other failing because the currentDestination is changed. This can happen if you double-tap, or click on two views with a click listener calling findNavController.navigate.

So to resolve this you can either use if-checks, try-catch or if you are interested there is a findSafeNavController() which does this checks for you before navigating. It also has a lint-check to make sure you don't forget about this issue.

GitHub

Article detailing the issue

Gergely Hegedus
  • 482
  • 3
  • 9
0

I created this extension function for Fragment:

fun Fragment.safeNavigate(
    @IdRes actionId: Int,
    @Nullable args: Bundle? = null,
    @Nullable navOptions: NavOptions? = null,
    @Nullable navigatorExtras: Navigator.Extras? = null
) {
    NavHostFragment.findNavController(this).apply {
        if (currentDestination?.label == this@safeNavigate::class.java.simpleName) {
            navigate(actionId, args, navOptions, navigatorExtras)
        }
    }
}
Mehmed
  • 2,880
  • 4
  • 41
  • 62
0

If you're using a recyclerview just add a click listener cooldown on your click and also in your recyclerview xml file use android:splitMotionEvents="false"

Crazy
  • 576
  • 6
  • 5
0

This error may have occured because you may have assigned the target screen to the wrong graph

Neuron
  • 1,020
  • 2
  • 13
  • 28
0

I was having this same issue in my projects, first I tried to debounce the clicks on the view that was triggering the navigation action, but after some experimenting I found that on really slow devices the debounce should be a very high value that causes the app to feel slow for users with fast devices.

So I came up with the following extensions for NavController, I think it is in line with the original API and is easy to use:

fun NavController.safeNavigate(directions: NavDirections) {
    try {
        currentDestination?.getAction(directions.actionId) ?: return
        navigate(directions.actionId, directions.arguments, null)
    } catch (e : Exception) {
        logError("Navigation error", e)
    }
}

fun NavController.safeNavigate(directions: NavDirections, navOptions: NavOptions?) {
    try {
        currentDestination?.getAction(directions.actionId) ?: return
        navigate(directions.actionId, directions.arguments, navOptions)
    } catch (e : Exception) {
        logError("Navigation error", e)
    }
}

fun NavController.safeNavigate(directions: NavDirections, navigatorExtras: Navigator.Extras) {
    try {
        currentDestination?.getAction(directions.actionId) ?: return
        navigate(directions.actionId, directions.arguments, null, navigatorExtras)
    } catch (e : Exception) {
        logError("Navigation error", e)
    }
}

Please note that I am using SafeArgs and NavDirections. These functions check if the action is valid from the current destination and only navigate if the action is not null. The try catch part should not be necessary if the Navigation library returns the correct action every time, but I wanted to eliminate all possible crashes.

-1

Answered in link: https://stackoverflow.com/a/67614469/5151336

Added extension function for Navigator for safe navigation.

vishnu benny
  • 998
  • 1
  • 11
  • 15