导航组件 Kotlin - 无法从当前目的地找到

68

我有一个碎片A、B、C。从A导航到B没问题,但是从B导航到C会崩溃。

这是我的导航:

在此输入图片描述

这是我的导航代码:

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

这是指向productItem目标的XML代码。

<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>

以下是错误信息:

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

我不知道发生了什么事情,但似乎navController正在寻找“navigation_home”


你能分享完整的nav_host.xml文件吗? - TRK P
@Cyd,删除那行代码 navController?.navigateUp() 可能会解决你的问题。 - Asad Mahmood
16个回答

47

这更像是提醒而不是答案,但希望能有所帮助。

总结:(正如其他人已经说过的)导航函数的连续调用是大多数异常的原因。

考虑到 Android 组件的结构,特别是 MediatorLiveData 的工作方式,人们有时可能会想要将数据节点加入单个可观察数据持有者(LiveData)中。

如果观察该中介器与动态导航函数相关联,那么肯定会出现错误。

原因是源可以连续更改 LiveData 值,次数等于连接到中介器的源的数量。

这是一个非常好的想法,但是对 NavController 的重复更改肯定会产生不良结果。

这可能包括:

  • 弹出后退堆栈两次。

  • 连续从 A 到 B 两次,第二次会出现“未找到 A”异常。

这是一个很大的测试问题,特别是由于一个 Fragment 的问题可能会级联到下层堆栈,因此当 Direction not found 异常在一个 Fragment 中出现时,真正的罪魁祸首可能在给出异常的 Fragment 上方的 Fragment 中找到。

实际上,可以通过创建一个具有延迟容忍度的自我取消线程执行器 scheduled.cancel(true); 来轻松解决这个问题,使其作用于 mediatorLiveData 本身(确切地说是onChange方法,而不是setValue(),因为对于MediatorLiveData来说,急切的内部状态更新是它的全部和唯一目的/笑话(抱歉,不允许使用 postValue()!)。

更不用说 mediator 本身就是一个不完整的组件了...

另一种更简单的方法是确保当且仅当 !Object::Equals 时才执行 MutableLiveData 的 onChange 调用,并防止重复调用 onChange(),这仍然证明了 MediatorLiveData/LiveData 的不完整性。(只需要特别小心处理 List)

无论如何都要避免连续调用 NavController,如果你非常必须使用它,则延迟的可运行程序可能是你唯一能够实现它的方式。


1
谢谢指出。我正在听导航更改的流,并在Fragments的onViewCreated中开始多次订阅它(该方法会被多次调用)。 - cwiesner

30
首先,当尝试检索您的导航控制器时,不应通过requireView()传递 - navController = Navigation.findNavController(requireView())。 您应该传递实际的导航宿主片段实例。
其次,问题是由于在片段A上时尝试从B -> C调用导航路径引起的。
您的方向路径是从B -> C。
val action = CategoryProductItemsDirections.actionCategoryProductItems2ToProductItem(null, it)

但是您首先向上导航,因此在尝试执行导航时实际上现在位于 Fragment A:

navController?.navigateUp()
navController?.navigate(action)

5
处理嵌套片段,如ViewPager时,你很容易犯一个错误,尝试从嵌套页面导航到另一个片段,而实际上导航必须是从ViewPager片段到目标片段。 - Jeffrey
有趣的评论,但不确定ViewPager与这个帖子/问题有什么关联?没有提到使用ViewPager。 - Indiana
2
你的回答帮助我解决了我在评论中提到的问题。也就是说,尝试从目前不在的目的地导航。 - Jeffrey

17

我创建了一个扩展函数来检查从当前目标开始执行操作的可行性。

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) }
    }
}}

orEmpty()部分是对Int?的扩展,如下所示:

fun Int?.orEmpty(default: Int = 0): Int {
    return this ?: default
}

1
在这行代码中,最后的.orEmpty()是红色的。我不知道为什么。如果(destinationId != EMPTY_INT) { currentNode?.findNode(destinationId)?.let { navigate(resId, args) } }在这个条件语句中,EMPTY_INT是红色的,它的值将被检查。请指导我。 - Syed Rafaqat Hussain
@SyedRafaqatHussain 看起来 .orEmpty() 是一种扩展,它将 int 值应用于 destinationId。您可以在不调用此扩展的情况下使用 destinationId 作为可空属性,并将检查更改为 if (destinationId != null)。 - Sviatoslav Zaitsev
1
非常好的解决方案。 - Luiz Alegria
1
但是,当您传递目标 resId 而不是操作 resId 时,此逻辑不起作用,因为 currentDestination?.getAction(resId) 返回 null。 - Shefchenko

6

//通过传递ID检查当前片段是否是事件触发片段

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

//在调用导航的片段中实现

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

它有效了,@Gobinath Nataraj - Tippu Fisal Sheriff

6

下面是NavigationUtils类的Java版本,用于安全导航:

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;
    }
}

您可以像这样使用这个类:
NavController navController = Navigation.findNavController(view);
NavigationUtils.navigateSafe(navController, R.id.action_firstFragment_to_secondFragment, null);

或者:
NavController navController = Navigation.findNavController(view);
NavDirections direction = FirstFragmentDirections.actionFirstFragmentToSecondFragment(yourModel, bundleArgs);
NavigationUtils.navigateSafe(navController, direction);

2
这对我的Kotlin代码库进行了一些适应,但主要思路是一样的。它确实帮了我很多。 - bazyle

2
这种错误通常出现在元素列表中,点击其中一个元素触发导航时。我用以下代码解决了这个问题:在调用导航函数之前,我会检查当前目的地是否是预期目的地
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
}

这确保了当目的地处于合法状态时才进行导航。[没有IllegalStateException...:))]


2

对于我的情况,我通过替换来解决了这个问题。

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

使用

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

1
在我的情况下,我只是将 lifecycleScope 更改为 viewLifecycleOwner.lifecycleScope,因为我在 Activity/Fragment 范围内注册了观察者,而不是使用单个 Fragment 范围注册观察者。这样可以防止多次启动导航。现在,您可以安全地使用以下代码:
        viewLifecycleOwner.lifecycleScope.launch {
            whenStarted {
                viewModel.someState.collect { state ->
                    when (state) {
                        SomeState.SUCCESS -> {
                            val action = ...
                            findNavController().navigate(action)
                        }
                    }
                }
            }
        }

1

使用@Homayoon Ahmadi的Java方法和@vishnu benny的orEmpty()扩展函数,可以通过传递NavDirections操作来安全地导航的Kotlin扩展函数。

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
}

1

不幸的是,基于NavigationUtils的解决方案,仅使用NavGraph类的findNode()方法,有一个严重的缺点 - 不可能导航到指向当前NavGraph本身的目标。换句话说,findNode()方法将找不到任何内容。
下面的图表中可以考虑作为示例操作action_startingFragment_to_startingFragment

<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>

为考虑到提到的情况,还需要检查找到的当前节点本身是否为目标终点。
因此,扩展函数将如下所示:
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)
                }
            }
        }
    }
}

更新:

还有一个更有趣的情况缺失,就是当目标指向父级NavGraph本身时,如下所示。

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)
}


使用findDestination将忽略编程错误,就像try-catch一样,所以我不知道它是否更好。 - arekolek

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接