参数异常:导航目标 xxx 在此 NavController 中未知。

226

我在使用新的Android Navigation Architecture组件时遇到了问题,当我尝试从一个片段导航到另一个时,我会收到这个奇怪的错误:

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

除了这个特定的导航之外,其他导航都正常工作。

我使用Fragment的 findNavController()函数来访问NavController

将不胜感激您的任何帮助。


请提供一些代码以便更好地理解。 - Alex
到目前为止,随着库的新版本的发布,这个bug的出现率已经降低了,但我认为这个库的文档还不够完善。 - Jerry Okafor
40个回答

5
在我的情况下,我有多个导航图文件,并且我想从一个导航图位置移动到另一个导航图的目标位置。
为此,我们必须像这样将第二个导航图包含在第一个导航图中:
<include app:graph="@navigation/included_graph" />

并将此添加到您的操作中:

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

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

在第二张图中。

更多信息在这里


5

在进行导航之前,您可以检查请求导航的片段是否仍然是当前目标,来自此代码片段

它基本上为后续查找在片段上设置了一个标记。

/**
 * 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 只是一个id,您需要将其添加到ids.xml文件中,以确保它是唯一的。 <item name="tag_navigation_destination_id" type="id" />

有关错误和解决方案以及“修复可怕的“... is unknown to this NavController”问题”中的navigateSafe(...)扩展方法的更多信息。


1
我研究了几种不同的解决方案,你的绝对是最好的。看到它得到如此少的关注让我感到难过。 - Luke Needham
1
NAV_DESTINATION_ID的位置上创建一个唯一标识符可能会很有用,可以使用类似于这个链接中的方法:https://dev59.com/2HE85IYBdhLWcg3wRBRU#15021758 - William Reed
标签从哪里来,为什么需要它?我遇到了问题,导航组件上的实际ID与R.id中的ID不匹配。 - riezebosch
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" /> - Frank
即使使用了这个解决方案,当弹出后退栈时仍然可能发生原始崩溃。您可能希望添加 fun Fragment.popBackStackSafe() { if (mayNavigate()) findNavController().popBackStack() } - Luke Needham

4
我写了这个扩展程序。
fun Fragment.navigateAction(action: NavDirections) {
    val navController = this.findNavController()
    if (navController.currentDestination?.getAction(action.actionId) == null) {
        return
    } else {
        navController.navigate(action)
    }
}

4

如其他答案所提到的,这个异常通常发生在用户:

  1. 在处理导航的同时,同时点击多个视图
  2. 在一个处理导航的视图上连续点击多次

使用计时器禁用点击不是解决此问题的适当方式。如果在计时器到期后用户没有被导航到目的地,应用程序将崩溃;在许多情况下,快速点击是必要的而且并非导航操作。

对于情况1,在xml中设置android:splitMotionEvents="false" 或者在源文件中设置 setMotionEventSplittingEnabled(false) 应该有所帮助。将这个属性设置为false 将只允许一个视图接受点击。你可以在这里了解更多。

对于情况2,会有一些东西延迟导航过程,使得用户可以在一个视图上连续点击(API调用,动画等)。根本问题应尽可能解决,以便导航即刻完成,不允许用户再次点击视图。如果延迟是不可避免的,比如在API调用的情况下,禁用视图或使其无法点击是适当的解决方案。


4

我曾通过在导航之前进行检查而不是使用立即单击控件的样板代码来解决相同的问题。

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

根据这个答案:

https://dev59.com/91QJ5IYBdhLWcg3wCxa3#56168225


3
在仔细考虑了Ian Lake在这个Twitter帖子中的建议后,我提出了以下方法。将NavControllerWrapper定义如下:
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()
}

然后在导航代码中:

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


Kotlin正是为此目的提供了扩展函数,无需使用包装器。 - Nicolas
当您在此处和那里使用扩展函数时,无法执行单元测试。相反,当您使用包装器时,您会在组件中引入一个接缝,因此您可以根据需要模拟组件并执行纯单元测试。 - azizbekian

3

看起来,混合使用fragmentManager控制返回堆栈和导航架构控制返回堆栈也可能导致这个问题。

例如,原始的CameraX基本示例使用了以下fragmentManager返回堆栈导航,似乎它没有正确地与导航交互:

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

如果您在从主碎片(本例中为相机碎片)移动之前记录“当前目标”,然后在返回主碎片时再次记录它,您可以从日志的id中看到id不同。预测是,导航在移动到碎片时更新了它,而fragmntManager在返回时没有再次更新它。从日志记录: 之前:D/CameraXBasic: currentDest?:androidx.navigation.fragment.FragmentNavigator$Destination@b713195 之后:D/CameraXBasic: currentDest?:androidx.navigation.fragment.FragmentNavigator$Destination@9807d8f
CameraX基本示例的更新版本使用Navigation进行返回。
 // Handle back button press
        view.findViewById<ImageButton>(R.id.back_button).setOnClickListener {
            Navigation.findNavController(requireActivity(), R.id.fragment_container).navigateUp()
        }

这个功能正常工作,日志显示在返回到主要片段时id相同。

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

After: D/CameraXBasic: 当前目标:androidx.navigation.fragment.FragmentNavigator$Destination@b713195

我怀疑故事的寓意,至少目前来看,就是要非常小心地将导航与fragmentManager导航混合使用。


这听起来很有道理,我会进一步调查。有人能够验证或证实这个说法吗? - Jerry Okafor
@JerryOkafor - 我已经在基于CameraX示例的应用程序中进行了测试并验证了它,但是如果有其他人也看到了这一点,那就太好了。实际上,在同一应用程序中,我在一个地方错过了“后退导航”,所以最近又修复了它。 - Mick
我可以证实“故事寓意”,但可能无法提供确切的代码。我最近接手了一个项目,其中使用 FragmentManager 来(错误地)获取“当前片段”,然后进行“安全导航”。这个答案让我感同身受,因为我注意到问题中的两个片段通常不相同(因此所谓的安全导航代码仍会导致应用程序崩溃)。感谢 Mick 的第一个建议,让我确认自己可能正在正确的轨道上。 - Richard Le Mesurier

3
在我的情况下,出现了bug,因为我在闪屏后启用了具有“Single Top”和“Clear Task”选项的导航操作。

2
但是 clearTask 已被弃用,您应该使用 popUpTo()。 - Jerry Okafor
@Po10cio 这些标志都是不必要的,我只是将它们删除了,问题已经解决。 - Eury Pérez Beltré

3

更新@AlexNuts答案,支持导航到嵌套图形。当操作使用嵌套图形作为目标时,例如:

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

无法将此操作的目标ID与当前目标进行比较,因为当前目标不能是图形化表示。嵌套图的起始目标必须得到解决。

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

然而,这将防止重复导航到相同的目标,有时是需要的。为了允许这样做,您可以添加一个检查到action.navOptions?.shouldLaunchSingleTop(),并在您不想要重复目标的操作中添加app:launchSingleTop="true"

3
为了避免这种崩溃,我的一位同事编写了一个小型库,它公开了一个 SafeNavController,这是 NavController 的包装器,并处理了因同时出现多个导航命令而导致此崩溃的情况。
这里有一篇关于整个问题和解决方案的简短文章
您可以在这里找到该库。

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