当我试图从一个片段导航到另一个片段时,我遇到了新的Android导航架构组件的问题,我得到了这个奇怪的错误:

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

其他导航都很好,除了这个。

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

任何帮助都将不胜感激。


当前回答

用try-catch(简单的方法)包装你的导航调用,或者确保在短时间内只有一个导航调用。这个问题可能不会消失。复制更大的代码片段在你的应用程序和尝试。

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

下面是导致我的应用程序崩溃的代码:

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

一个很容易重现这个错误的方法是用多个手指在项目列表上点击,点击每个项目就会在导航到新屏幕上解决(基本上和人们注意到的一样——在很短的时间内点击两次或两次以上)。我注意到:

第一次导航调用总是正常工作; 第二个和所有其他的导航方法调用在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时,我们使用相同的导航图、导航控制器和后堆栈。

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.

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

其他回答

在我的例子中,如果用户非常非常快地点击同一个视图两次,这个崩溃就会发生。所以你需要实现某种逻辑来防止多次快速点击……这很烦人,但似乎是必要的。

你可以在这里阅读更多关于防止这种情况的内容:Android防止双击按钮

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

我在我的项目中也有同样的问题,首先我试图在视图上触发导航动作的点击,但经过一些实验后,我发现在真正缓慢的设备上,debounce应该是一个非常高的值,导致应用程序对快速设备的用户感到缓慢。

所以我为NavController提出了以下扩展,我认为它符合原始的API,并且易于使用:

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

请注意,我正在使用SafeArgs和NavDirections。这些函数检查操作从当前目的地是否有效,并且仅在操作不为空时进行导航。如果Navigation库每次都返回正确的操作,那么try catch部分就不需要了,但我希望消除所有可能的崩溃。

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

当我按了两次后退键时,我想到了这个问题。首先,我拦截KeyListener并覆盖KeyEvent.KEYCODE_BACK。我在名为OnResume的函数中添加了下面的代码,然后解决了这个问题/问题。

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

当我第二次遇到这种情况时,它的状态与第一次相同,我发现我可能会使用add函数。让我们来分析一下这些情况。

首先,FragmentA导航到FragmentB,然后FragmentB导航到FragmentA,然后按下返回按钮…崩溃出现了。 其次,FragmentA导航到FragmentB,然后FragmentB导航到FragmentC, FragmentC导航到FragmentA,然后按下返回按钮…崩溃出现了。

所以我认为当按下返回按钮时,FragmentA会回到FragmentB或FragmentC,这会导致登录混乱。最后,我发现名为popBackStack的函数可以用于返回,而不是导航。

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

到目前为止,问题已经真正解决了。

我写了这个扩展

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