63

I have a fragment A,B,C. Its okay when navigating from A -> B, but from B -> C it crashes.

Here is my Navigation

enter image description here

Here is my navigation code

 categoryProductItemListAdapter.setOnItemClickListener {
        val action = CategoryProductItemsDirections.actionCategoryProductItems2ToProductItem(null, it)
        navController = Navigation.findNavController(requireView())
        navController?.navigateUp()
        navController?.navigate(action)
    }

Here is the XML code for the destination to productItem

<fragment
    android:id="@+id/categoryProductItems2"
    android:name="com.sample.store.main.dashboard.ui.ui.home.categoryitems.CategoryProductItems"
    android:label="CategoryProductItems"
    tools:layout="@layout/fragment_category_product_items">
    <argument
        android:name="category_global"
        app:argType="com.sample.store.data.globalmodels.response.categories.Category" />
    <action
        android:id="@+id/action_categoryProductItems2_to_productItem"
        app:destination="@id/productItem"
        app:enterAnim="@anim/enter_from_right"
        app:exitAnim="@anim/exit_to_right"
        app:popEnterAnim="@anim/fragment_open_enter"
        app:popExitAnim="@anim/fragment_fade_exit" />
</fragment>

And here is the error:

java.lang.IllegalArgumentException: Navigation action/destination com.sample.store.full:id/action_categoryProductItems2_to_productItem cannot be found from the current destination Destination(id/navigation_home) label=Home class=com.sample.store.main.dashboard.ui.ui.home.mainui.HomeFragment

I don't know what happened, but it seems that the navController is looking for the "navigation_home"

Cyd
  • 1,245
  • 1
  • 14
  • 17

13 Answers13

47

This is more a heads up than an answer. But I hope it helps.

Summary: (As others have already said:) Successive calls to Navigation functions are the reason of most of these exceptions.

Given how the android components are structured, specially how MediatorLiveData works, people may sometimes want to join data nodes in a single observable data holder (LiveData).

If an observation of this mediator is linked to dynamic Navigation functions, bugs will undoubtedly appear.

The reason is that sources may change a LiveData value a successive number of times equal to the amount of sources connected to the Mediator.

This is a perfectly good idea, BUT. Repeated changes to the NavController will definitely result in undesirable outcomes.

This may include:

  • popping the backStack twice.

  • Going from A -> B twice in a row giving an exception of A not found
    the second time.

This is a big testing problem specially since the issue of one Fragment may cascade to the underlaying stacks, and so when an exception of Direction not found may arise in one Fragment, The real culprit may be found on the Fragment on TOP of the one giving the exception.

In reality this would be easily solved by creating a self cancelling thread executor scheduled.cancel(true); with a delaying tolerance on the mediatorLiveData itself (the onChange to be precise, not the setValue() since eager inner state updates are the entire and only purpose/joke of the mediator IMHO (sorry, no postValue() allowed!)).

Not to mention that the mediator itself is an incomplete component...

Another easier approach is to make sure that onChange calls from a MutableLiveData are performed if and only if !Object::Equals, and prevent repeating calls to onChange() which is still a testament of the incompleteness of the MediatorLiveData/LiveData. (Just be extremely careful with Lists)

At all costs avoid performing successive calls to a NavController, and if you somehow MUST, then a delayed runnable may be your only way to achieve it.

Delark
  • 1,141
  • 2
  • 9
  • 15
  • 1
    thanks for pointing that out. i was listening to a flow for navigation changes and was subscribing to it multiple times as i started listening in Fragments onViewCreated (which is called multiple times) – cwiesner Mar 03 '22 at 10:48
29

Firstly you should not pass requireView() when trying to retrieve your Nav controller - navController = Navigation.findNavController(requireView()). You should be passing the actual Navigation Host Fragment instance.

Secondly the issue is being caused because you are trying to call a Navigation path from B -> C, when on fragment A.

Your direction path is from B -> C

val action = CategoryProductItemsDirections.actionCategoryProductItems2ToProductItem(null, it)

But you navigate up first so you are actually now on Fragment A when trying to execute the navigation:

navController?.navigateUp()
navController?.navigate(action)
Indiana
  • 683
  • 7
  • 18
  • 5
    When dealing with nested fragments like a ViewPager you can easily make the mistake of trying to navigate from a nested page to another fragment when in fact the navigation must be from the ViewPager fragment to the destination fragment. – Jeffrey Jul 23 '21 at 00:47
  • Interesting comment, buy not sure what relevance a view pager has to this post / question? There's no mention of a view pager being used – Indiana Jul 24 '21 at 05:53
  • 2
    Your answer helped me solve the problem I mentioned in the comment. That is, trying to navigate from a destination that you are not currently on. – Jeffrey Jul 25 '21 at 03:04
17

I have created an extension function to check the feasibility of starting an action from the current destination.

fun NavController.navigateSafe(@IdRes resId: Int, args: Bundle? = null) {
val destinationId = currentDestination?.getAction(resId)?.destinationId.orEmpty()
currentDestination?.let { node ->
    val currentNode = when (node) {
        is NavGraph -> node
        else -> node.parent
    }
    if (destinationId != 0) {
        currentNode?.findNode(destinationId)?.let { navigate(resId, args) }
    }
}}

And the orEmpty() part is extension over Int? as follows:

fun Int?.orEmpty(default: Int = 0): Int {
    return this ?: default
}
vishnu benny
  • 998
  • 1
  • 11
  • 15
  • 1
    val destinationId = currentDestination?.getAction(resId)?.destinationId.orEmpty() In this line the last .orEmpty() is red. i dont know why. if (destinationId != EMPTY_INT) { currentNode?.findNode(destinationId)?.let { navigate(resId, args) } } and in this condition EMPTY_INT is red which value will be check there. guide me – Syed Rafaqat Hussain Aug 13 '21 at 06:10
  • @SyedRafaqatHussain looks like .orEmpty() is some extention which apply int value to destinationId. You can use destinationId as nullable property without this extention call and just change check to if (destinationId != null) – Sviatoslav Zaitsev Feb 17 '22 at 22:09
  • 1
    very good solution. – Luiz Alegria Apr 27 '22 at 19:49
  • 1
    But when you pass a destination resId instead of action resId, this logic is not working, because `currentDestination?.getAction(resId)` is returning null. – Shefchenko May 17 '22 at 15:02
5

Here is Java version of NavigationUtils class for safe navigation:

public abstract class NavigationUtils {

    /**
     * This function will check navigation safety before starting navigation using direction
     *
     * @param navController NavController instance
     * @param direction     navigation operation
     */
    public static void navigateSafe(NavController navController, NavDirections direction) {
        NavDestination currentDestination = navController.getCurrentDestination();

        if (currentDestination != null) {
            NavAction navAction = currentDestination.getAction(direction.getActionId());

            if (navAction != null) {
                int destinationId = orEmpty(navAction.getDestinationId());

                NavGraph currentNode;
                if (currentDestination instanceof NavGraph)
                    currentNode = (NavGraph) currentDestination;
                else
                    currentNode = currentDestination.getParent();

                if (destinationId != 0 && currentNode != null && currentNode.findNode(destinationId) != null) {
                    navController.navigate(direction);
                }
            }
        }
    }


    /**
     * This function will check navigation safety before starting navigation using resId and args bundle
     *
     * @param navController NavController instance
     * @param resId         destination resource id
     * @param args          bundle args
     */
    public static void navigateSafe(NavController navController, @IdRes int resId, Bundle args) {
        NavDestination currentDestination = navController.getCurrentDestination();

        if (currentDestination != null) {
            NavAction navAction = currentDestination.getAction(resId);

            if (navAction != null) {
                int destinationId = orEmpty(navAction.getDestinationId());

                NavGraph currentNode;
                if (currentDestination instanceof NavGraph)
                    currentNode = (NavGraph) currentDestination;
                else
                    currentNode = currentDestination.getParent();

                if (destinationId != 0 && currentNode != null && currentNode.findNode(destinationId) != null) {
                    navController.navigate(resId, args);
                }
            }
        }
    }

    private static int orEmpty(Integer value) {
        return value == null ? 0 : value;
    }
}

You can use this class like this:

NavController navController = Navigation.findNavController(view);
NavigationUtils.navigateSafe(navController, R.id.action_firstFragment_to_secondFragment, null);

or:

NavController navController = Navigation.findNavController(view);
NavDirections direction = FirstFragmentDirections.actionFirstFragmentToSecondFragment(yourModel, bundleArgs);
NavigationUtils.navigateSafe(navController, direction);
Homayoon Ahmadi
  • 1,181
  • 1
  • 12
  • 24
  • 2
    This worked for me with few adaptations to my kotlin codebase, but the main idea is the same. It really helped me actually. – bazyle Aug 04 '22 at 16:52
5

//Check whether the current fragment is event triggered fragment by passing the id

 fun Fragment.findNavControllerSafely(id: Int): NavController? {
        return if (findNavController().currentDestination?.id == id) {
            findNavController()
        } else {
            null
        }
    }

//Implement in fragment where you are invoking the navigate

findNavControllerSafely(R.id.fragment1)?.navigate(
                    R.id.action_fragment1_to_fragment2, bundle
                )
2

For my case,I resolve the problem by replacing

implementation "android.arch.navigation:navigation-fragment-ktx:1.0.0"

with

implementation "androidx.navigation:navigation-fragment-ktx:2.3.5"
2

This kind of error appears mostly in a list of elements and clicking on a item triggers navigation.

I solved with this code, on item click before calling navigate function I'll check whether the current destination is the intended one as,

val currentDestinationIsHome = this.findNavController().currentDestination == this.findNavController().findDestination(R.id.nav_home)
val currentDestinationIsDetail = this.findNavController().currentDestination == this.findNavController().findDestination(R.id.nav_detail)

if(currentDestinationIsHome && !currentDestinationIsDetail){
    .... 
    // perform navigation
}

This ensures navigation is only done when the destinations are in a legal state. [No IllegalStateException ... :)) ]

Willey Hute
  • 939
  • 13
  • 18
1

Kotlin extension function to safely navigate by passing a NavDirections action using @Homayoon Ahmadi Java method and @vishnu benny's orEmpty() extension function.

fun NavController.navigateSafely(direction: NavDirections) {
    val currentDestination = this.currentDestination
    if (currentDestination != null) {
        val navAction = currentDestination.getAction(direction.actionId)
        if (navAction != null) {
            val destinationId: Int = navAction.destinationId.orEmpty()
            val currentNode: NavGraph? = if (currentDestination is NavGraph) currentDestination else currentDestination.parent
            if (destinationId != 0 && currentNode != null && currentNode.findNode(destinationId) != null) {
                this.navigate(direction)
            }
        }
    }
}

fun Int?.orEmpty(default: Int = 0): Int {
    return this ?: default
}
M.Ed
  • 969
  • 10
  • 12
1

Unfortunately, solutions based on NavigationUtils, that use only findNode() method of NavGraph class, have one serious downside - it is not possible to navigate to destination that points to current NavGraph itself. Another words, findNode() method will find nothing.
As an example action action_startingFragment_to_startingFragment in the graph below can be considered:

<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/some_graph"
    app:startDestination="@id/startingFragment">

    <fragment
        android:id="@+id/startingFragment"
        android:name="com.xxx.StartingFragment">

        <action
            android:id="@+id/action_startingFragment_to_startingFragment"
            app:destination="@id/some_graph"
            app:popUpTo="@id/startingFragment"
            app:popUpToInclusive="true" />

    </fragment>

</navigation>

To take into account mentioned case, it is also required to check if found current node itself is a target destination or not.
So the extension function will look like the following:

fun NavController.navigateSafe(@IdRes actionId: Int, args: Bundle?) {
    currentDestination?.let { currentDestination ->
        val navAction = currentDestination.getAction(actionId)
        // to navigate successfully certain action should be explicitly stated in nav graph
        if (navAction != null) {
            val destinationId = navAction.destinationId
            if (destinationId != 0) {
                val currentNode = currentDestination as? NavGraph ?: currentDestination.parent
                if (currentNode?.id == destinationId ||     <--------- THIS CONDITION IS THE KEY
                    currentNode?.findNode(destinationId) != null
                ) {
                    navigate(actionId, args, null)
                }
            }
        }
    }
}

UPDATE:

Also more interesting case is lacking, when destination points to parent NavGraph itself like below.

Parent graph:

<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/main_graph"
    app:startDestination="@id/mainFragment">

    <fragment
        android:id="@+id/mainFragment"
        android:name="com.xxx.main.MainFragment">

        <action
            android:id="@+id/action_main_to_some"
            app:destination="@id/some_graph"/>

    </fragment>

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

</navigation>

Child graph:

<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/some_graph"
    app:startDestination="@id/someFragment">

    <fragment
        android:id="@+id/someFragment"
        android:name="com.xxx.some.SomeFragment">

        <action
            android:id="@+id/action_some_to_main"
            app:destination="@id/main_graph"
            app:popUpTo="@id/someFragment"
            app:popUpToInclusive="true" />

    </fragment>

</navigation>

Action action_some_to_main is target one.
Therefore, extension function need to be modified:

fun NavController.navigateSafe(@IdRes actionId: Int, args: Bundle?) {
    currentDestination?.let { currentDestination ->
        val navAction = currentDestination.getAction(actionId)
        // to navigate successfully certain action should be explicitly stated in nav graph
        if (navAction != null) {
            val destinationId = navAction.destinationId
            if (destinationId != 0) {
                val currentNode = currentDestination as? NavGraph ?: currentDestination.parent
                if (currentNode?.findDestination(destinationId) != null) { <----- CHANGED HERE
                    navigate(actionId, args, null)
                }
            }
        }
    }
}

private fun NavGraph.findDestination(destinationId: Int): NavDestination? {
    if (id == destinationId) return this
    val node = findNode(destinationId)
    if (node != null) return node
    return parent?.findDestination(destinationId)
}
gtgray
  • 111
  • 1
  • 6
  • using findDestination will ignore programming errors, just like try-catch, so I don't know if it's any better – arekolek Apr 17 '23 at 12:03
1

In my case, I just changed lifecycleScope to viewLifecycleOwner.lifecycleScope, because I registered observer in Activity/Fragment scope instead of registering observer with single fragment scope. It prevents to launch navigation two or more times. Now, You can safely use:

        viewLifecycleOwner.lifecycleScope.launch {
            whenStarted {
                viewModel.someState.collect { state ->
                    when (state) {
                        SomeState.SUCCESS -> {
                            val action = ...
                            findNavController().navigate(action)
                        }
                    }
                }
            }
        }
KurdTt-
  • 449
  • 1
  • 5
  • 21
0

For my case, I resolve the problem by replacing -

 <action
    android:id="@+id/action_categoryProductItems2_to_productItem"
    app:destination="@id/productItem"
    app:enterAnim="@anim/enter_from_right"
    app:exitAnim="@anim/exit_to_right"
    app:popEnterAnim="@anim/fragment_open_enter"
    app:popExitAnim="@anim/fragment_fade_exit"/>

with

 <action
    android:id="@+id/action_categoryProductItems2_to_productItem"
    app:destination="@id/productItem"
    app:enterAnim="@anim/enter_from_right"
    app:exitAnim="@anim/exit_to_right"
    app:popEnterAnim="@anim/fragment_open_enter"
    app:popExitAnim="@anim/fragment_fade_exit"
    app:popUpToInclusive="true" /* If true then also remove the destination from stack while popup */
    app:popUpTo="@id/navigation_home"/>  /*The fragment where to land again from destination*/
             
Gk Mohammad Emon
  • 6,084
  • 3
  • 42
  • 42
0

i found destinationId returns null when navController couldn't find destination.

if you are using navSafeArgs, it will work

fun NavController.navigateSafely(directions:NavDirections){
    currentDestination?.getAction(directions.actionId)?.destinationId?:return
    navigate(directions.actionId,directions.arguments,null)
}

Moon
  • 3
  • 2
-2

You need to set defaultNavHost="true". Like in this example:

<LinearLayout
            android:layout_width="match_parent"
            android:layout_height="match_parent"
            android:orientation="vertical">
            <androidx.fragment.app.FragmentContainerView
                android:id="@+id/myNavHostFragment"
                android:name="androidx.navigation.fragment.NavHostFragment"
                android:layout_width="match_parent"
                android:layout_height="match_parent"
                app:defaultNavHost="true"
                app:navGraph="@navigation/navigation_layout" />
        </LinearLayout>

Also dont forget to set the home activity in your navigation component.

Branddd
  • 78
  • 12