参数异常:导航目标 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个回答

130
在我的情况下,如果用户非常快地连续点击相同的视图,就会出现崩溃。因此,您需要实现某种逻辑来防止多次快速点击......这非常令人讨厌,但似乎是必要的。
您可以在此处了解更多有关如何防止此问题的信息:Android Preventing Double Click On A Button 编辑3/19/2019:为了进一步澄清,这个崩溃不仅仅是通过“非常快地连续点击相同的视图”来重现的。或者,您可以使用两个手指同时单击两个(或更多)视图,每个视图都有自己的导航。当您有一系列项目时,这特别容易做到。上述有关多次点击预防的信息将处理此情况。 编辑4/16/2020:如果您不太想阅读上面那篇Stack Overflow文章,我包括了我一直在使用的自己的(Kotlin)解决方案。

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

37
关于使用两个手指同时点击两个视图的修改!这对我来说是关键,帮助我轻松地复制了这个问题。非常感谢提供这个信息更新。 - Richard Le Mesurier
在调试阶段,当应用程序卡在等待继续执行时,我碰巧点击了一下。看起来又是IDE中连续两次点击的情况。 - marcolav
1
谢谢你。这让我避免了一些崩溃和头痛 :) - ibyte
9
这个解决方案是为了绕开导航组件的真正问题而设计的,但在速度较慢的设备上容易失败。创建和填充新片段可能需要超过200毫秒的时间。在延迟之后,可能会发送第二个点击事件,此时片段尚未显示,我们就回到了同样的问题。 - Nicolas
我的应用程序似乎并没有真正起作用。我仍然可以快速点击以至少在调试构建中产生崩溃。看起来是线程安全的问题,尽管实际上只应该有一个UI线程。奇怪。 - The incredible Jan

109

在调用导航之前检查 currentDestination 可能会有所帮助。

例如,如果你的导航图中有两个片段目标 fragmentAfragmentB,并且从 fragmentAfragmentB 只有一个操作。当你已经在 fragmentB 时调用 navigate(R.id.action_fragmentA_to_fragmentB) 将导致 IllegalArgumentException。因此,在导航之前应始终检查 currentDestination

if (navController.currentDestination?.id == R.id.fragmentA) {
    navController.navigate(R.id.action_fragmentA_to_fragmentB)
}

3
我有一个搜索应用程序,可以通过带参数的操作进行导航。因此它可以从当前目标导航到自身。我最终也是这样做的,除了 navController.currentDestination == navController.graph.node。虽然感觉有点不太妥当,我觉得我不应该这样做。 - Shawn Maybush
169
图书馆不应该强制我们进行这个检查,这实在是荒谬的。 - DaniloDeQueiroz
我有同样的问题。我有一个EditText和一个“保存”按钮,用于将EditText的内容存储到数据库中。每次按“保存”按钮时,它都会崩溃。我怀疑问题原因是,为了能够按下“保存”按钮,我需要通过点击返回按钮来关闭屏幕键盘。 - The Fox
2
即使在iOS中,当您多次按下按钮时,有时会推送多个ViewController。猜测Android和iOS都有这个问题。 - coolcool1994
我该如何在ViewPager内的片段中使用此解决方案,因为我没有目标ID? - Omar Beshary
显示剩余4条评论

67

您可以在导航控制器的当前目标中检查请求的操作。

更新添加了全局操作的使用,以实现安全导航。

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

1
这个解决方案对于定义在currentDestination的动作列表之外的任何操作都不起作用。比如说,你定义了一个全局动作并使用该动作进行导航,这将失败,因为该动作未在当前目标的<action>列表中定义。添加类似于currentDestination?.getAction(resId) != null || currentDestination?.id != resId的检查应该可以解决问题,但也可能无法覆盖每种情况。 - wchristiansen
@wchristiansen,感谢您的笔记。我已经更新了代码,使用了全局操作。 - Alex Nuts
1
@AlexNuts 答案很棒。我认为你可以删除 ?: graph.getAction(resId) -> currentDestination?.getAction(resId),将会返回全局或非全局操作的动作(我已经测试过了)。此外,最好使用 Safe Args -> 而不是单独传递 resIdargs,而是传递 navDirections: NavDirections - Wess
1
@AlexNuts 注意,此解决方案不支持导航到与当前目标相同的目标。换句话说,无法从目标X使用Bundle Y导航到具有Bundle Z的目标X。 - Wess
如果操作导航到嵌套图形,则此操作将失败。在这种情况下,操作目标ID将是嵌套图形的ID,而不是其起始目标的ID,从而绕过检查。 - Nicolas
1
谢谢你的想法!但是从我的角度来看,只需在navigateSafe包装器中处理异常就容易得多。我最终采用了以下解决方案:https://vadzimv.dev/2021/07/26/android-jetpack-navigation-navigate-safe.html - VadzimV

55

为了避免崩溃,我所做的是以下内容:

我有一个BaseFragment,在其中添加了以下fun以确保currentDestination知道destination

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

需要注意的是,我正在使用SafeArgs插件。


3
这应该是被采纳的答案。被采纳的答案不支持导航到对话框。 - Marek Teuchner
2
我认为这是这里最好的答案,谢谢。 - Daniel Wilson
1
非常感谢您的回答,我已经遇到这个问题大约一个月了。 - Randy Reiza

21

如果您具有一个包含Fragments B的ViewPager的Fragment A,并且您尝试从B导航到C,则也可能发生这种情况。

由于在ViewPager中,Fragments不是A的目标位置,因此您的图表不会知道您正在B上。

一种解决方法是在B中使用ADirections来导航到C。


在这种情况下,崩溃并不是每次都发生,只是偶尔发生。如何解决? - Srikar Reddy
您可以在navGraph中添加一个全局操作并使用它进行导航。 - Abraham Mathew
1
由于B不需要知道其确切的父级,最好使用ADirections通过接口进行操作,例如(parentFragment as? XActionListener)?.Xaction()。请注意,如果有帮助,您可以将此函数保存为局部变量。 - hmac
你能否分享一段示例代码来说明这个问题,因为我也遇到了同样的问题。 - Ikhiloya Imokhai
有人能提供一段示例代码吗?我卡在同一个问题上了。需要一个片段和一个选项卡片段。 - Usman Zafer

18

TL;DR 导航控制器的变化比UI更快,您在两个不同的状态下向导航控制器发送了两个相同的navigate(R.id.destn_id)

为了解决这个问题,可以将navigate调用包装在try-catch中(简单方法),或者确保在短时间内只有一个navigate调用。这个问题可能不会消失。在应用程序中复制更大的代码片段并尝试。

你好。基于上面几个有用的回答,我想分享我的解决方案,可以进行扩展。

以下是在我的应用程序中导致崩溃的代码:

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

一种轻松重现错误的方法是在项目列表上用多个手指轻敲,每次点击都会导航到新屏幕(基本上与人们注意到的相同-在非常短的时间内进行两次或更多次点击)。 我注意到:
  1. 第一个 navigate 调用总是正常工作的;
  2. 第二个及所有其他 navigate 方法的调用都会导致 IllegalArgumentException
从我的角度来看,这种情况可能经常出现。 由于重复代码是不良实践,而且始终具有一个影响点是很好的,因此我想到了下一个解决方案:
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.");
    }
}

}

因此,上面的代码只有一行发生了变化:

Navigation.findNavController(recyclerView).navigate(R.id.action_listFragment_to_listItemInfoFragment, bundle);

转换为:

NavigationHandler.navigate(recyclerView, R.id.action_listFragment_to_listItemInfoFragment, bundle);

它甚至变得稍微短了一点。代码已在崩溃发生的确切位置进行了测试。不再遇到此问题,并将使用相同的解决方案来避免其他导航中出现相同的错误。

欢迎任何想法!

是什么导致了崩溃

请记住,当我们使用方法Navigation.findNavController时,我们在使用相同的导航图、导航控制器和后退栈。

我们总是在这里获得相同的控制器和图形。当调用navigate(R.id.my_next_destination)时,图形和后退栈几乎立即更改,而UI尚未更新。只是不够快,但没关系。后退栈更改后,导航系统接收到第二个navigate(R.id.my_next_destination)调用。由于后退栈已更改,因此我们现在相对于堆栈中的顶部片段进行操作。顶部片段是您通过使用R.id.my_next_destination导航到的片段,但它不包含具有IDR.id.my_next_destination的下一个任何目的地。因此,由于该片段对ID一无所知,您会收到IllegalArgumentException

此确切错误可以在NavController.java方法findDestination中找到。


很长时间没有尝试过了。但我想这仍然是一个问题。 你可以尝试强制调用 navigate 函数两次,看看这是否仍然是一个问题。只需将其一行接着一行写即可。 - Jenea Vranceanu
我尝试调用“navigate”两次,结果导致了崩溃。根据谷歌的说法,这是合理的,因为它会立即执行,所以不允许我从https://issuetracker.google.com/issues/274016275#comment2进行跳转。我理解这一点,但我仍然希望在控制范围之外时能够避免此类问题。我关心的是其他情况,有些原因可能导致我无法进行导航。有人告诉我,如果我在onResume中,那么就应该是安全的,但实际上这是不正确的:issuetracker.google.com/issues/273978797 - android developer
我已经阅读了您提供的问题跟踪器上的帖子。说实话,我认为在当前导航库的实现中,没有一个“安全”的位置可以调用navigate函数。这是使用全局状态(NavigationController在幕后处理)和由调用navigate引起的副作用的完美例子。我真诚地相信,除非Android开发团队在深入思考之后解决这个问题,否则这个问题不会得到解决。 - Jenea Vranceanu
我想他们可能会提供一个解决方案。例如(仅仅是我脑海中的一个想法):当调用 findNavController 时,它应该返回一个导航控制器,其中包含代表您在调用该函数的当前片段或活动状态的一些特定信息。因此,正如您在问题跟踪器上讨论的那样,它将为开发人员提供足够的信息来决定是否应该调用 navigation 函数。 - Jenea Vranceanu
我不确定,但也许这可以告诉你是否可以到达新目的地:newDestinationId=currentBackStackEntry.destination.getAction(navigationActionId)?.destinationId。如果这是空的,你就不能使用当前位置的导航操作ID。在正常情况下,你可以使用 newDestination = newDestinationId?.let { currentNode.parent?.findNode(newDestinationId) }。我不知道这是否总是有帮助的,但他们应该为我们提供一些函数来帮助我们进行导航,并使其更容易和更短。 - android developer
显示剩余4条评论

15

请尝试以下步骤:

  1. 创建以下扩展函数(或普通函数):

更新(不使用反射并且更易读)

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

旧版(带反射)

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. 从您的Fragment中使用如下方式:
val direction = FragmentOneDirections.actionFragmentOneToFragmentTwo()
// new usage
safeNavigateFromNavController(direction)

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

我的问题是

我有一个片段(FragmentOne),它转到另外两个片段(FragmentTwo和FragmentThree)。在一些低端设备上,用户按下按钮会重定向到FragmentTwo,但在几毫秒后,用户按下按钮会重定向到FragmentThree。结果是:

致命异常:java.lang.IllegalArgumentException Navigation action/destination action_fragmentOne_to_fragmentTwo 无法从当前目标中找到 Destination(fragmentThree) class=FragmentThree

我的解决方案是:

我检查当前目标是否属于当前片段。如果是真的,我执行导航操作。

就是这样!


1
@AlexShevchyshen 我发布了一个更新,没有使用反射机制,更易读。 - Abner Escócio
在这个扩展函数中,您必须传递navDirections对象,而不是action safeNavigateFromNavController(navDirection)。+1 - Daniyal Javaid

14

在我的情况下,我正在使用自定义的返回按钮进行导航。我调用了onBackPressed()而不是以下代码

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

这导致了IllegalArgumentException的发生。之后我改用navigateUp()方法,就再也没有崩溃了。


我不明白onBackPressed和this之间的区别,仍然卡在系统返回按钮上,覆盖它并用this替换似乎很疯狂。 - Daniel Wilson
2
我同意这似乎很疯狂。在Android导航架构组件中遇到的许多事情都感觉有点疯狂,它的设置过于严格,在我看来。考虑为我们的项目做自己的实现,因为它只会带来太多的问题。 - the-ginger-geek
对我没用……仍然得到相同的错误。 - Otziii

7
在我的案例中,问题出现在我将一个Fragment作为viewpager fragment的子项并重复使用时。 viewpager Fragment(作为父Fragment)被添加到了Navigation xml中,但是在viewpager父Fragment中没有添加action。
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">

通过向父viewpager片段添加动作来解决问题,如下所示:
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>

6

今天

def navigationVersion = "2.2.1"

问题仍然存在,我的Kotlin解决方案是:

// To avoid "java.lang.IllegalArgumentException: navigation destination is unknown to this NavController", se more https://dev59.com/mVUK5IYBdhLWcg3w9jrb
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)
    }
}

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