IllegalArgumentException: пункт назначения xxx неизвестен этому NavController

У меня проблема с новым компонентом архитектуры навигации Android, когда я пытаюсь перейти с от одного фрагмента к другому, я получаю эту странную ошибку:

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

Любая другая навигация работает нормально, кроме этой.

Я использую функцию фрагмента findNavController(), чтобы получить доступ к NavController.

Любая помощь будет оценена по достоинству.

Пожалуйста, предоставьте код для лучшего понимания.

Alex 27.06.2018 20:54

До сих пор частота появления этой ошибки была снижена с новыми выпусками библиотеки, но я думаю, что библиотека еще недостаточно хорошо документирована.

Jerry Okafor 21.09.2018 09:44
190
2
69 281
38

Ответы 38

В моем случае ошибка возникла из-за того, что у меня было действие навигации с включенными опциями Single Top и Clear Task после экрана-заставки.

Но clearTask устарел, вместо него следует использовать popUpTo ().

Jerry Okafor 21.09.2018 09:43

@ Po10cio Ни один из этих флагов не понадобился, я просто удалил его, и он был исправлен.

Eury Pérez Beltré 24.09.2018 14:44

Похоже, вы решаете задачу. Приложение может иметь одноразовую настройку или серию экранов входа в систему. Эти условные экраны не следует рассматривать как начальную точку назначения вашего приложения.

https://developer.android.com/topic/libraries/architecture/navigation/navigation-conditional

В моем случае я использовал настраиваемую кнопку возврата для перехода вверх. Я позвонил onBackPressed() вместо следующего кода

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

Это вызвало появление IllegalArgumentException. После того, как я изменил его на использование метода navigateUp(), у меня больше не было сбоев.

Я не понимаю, в чем разница между onBackPressed и этим, все еще застрял на кнопке возврата системы и переопределил ее и заменил это кажется сумасшедшим

Daniel Wilson 04.02.2019 14:22

Я согласен, это кажется безумным. Многие вещи, с которыми я столкнулся в компоненте архитектуры навигации Android, кажутся немного сумасшедшими, они слишком жестко настроены IMO. Думаю о собственной реализации для нашего проекта, потому что это создает слишком много головной боли

the-ginger-geek 05.02.2019 10:31

У меня не работает ... По-прежнему возникает та же ошибка.

Otziii 07.05.2019 15:28

В моем случае, если пользователь дважды очень быстро щелкнет одно и то же представление, произойдет сбой. Поэтому вам нужно реализовать какую-то логику, чтобы предотвратить множественные быстрые щелчки ... Что очень раздражает, но, похоже, необходимо.

Вы можете прочитать больше о том, как предотвратить это здесь: Android: предотвращение двойного щелчка по кнопке

Изменить 19 марта 2019 г.: Чтобы уточнить немного больше, этот сбой не может быть воспроизведен исключительно простым «очень быстрым щелчком по одному и тому же виду». В качестве альтернативы вы можете просто использовать два пальца и одновременно щелкнуть два (или более) представления, при этом каждое представление имеет свою собственную навигацию, которую они будут выполнять. Это легко сделать с помощью особенно, когда у вас есть список элементов. Вышеупомянутая информация о предотвращении множественных кликов поможет в этом случае.

Изменить 16.04.2020: На всякий случай, если вам не очень интересно читать этот пост о переполнении стека выше, я включаю свое собственное (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
    }
}

Редактирование об использовании 2-х пальцев и одновременном нажатии 2-х представлений! Это ключ для меня и помог мне легко воспроизвести проблему. Отличное обновление с этой информацией.

Richard Le Mesurier 11.04.2019 18:19

Во время фазы отладки я случайно щелкнул, когда приложение зависло в ожидании продолжения выполнения. Похоже на еще один случай двух последовательных нажатий на IDE

marcolav 22.08.2019 22:54

Спасибо за это. Спас меня от нескольких вылетов и головной боли :)

user2672052 27.04.2020 14:09

Это решение - хитрость, позволяющая обойти настоящую проблему: компонент навигации. Он также склонен к сбою на более медленных устройствах. Создание и наполнение нового фрагмента может занять более 200 мс. После этой задержки может быть отправлено второе событие щелчка, прежде чем фрагмент будет показан, и мы вернемся к той же проблеме.

Nicolas 16.08.2020 23:15

На самом деле не работает в моем приложении. Я все еще могу щелкать достаточно быстро, чтобы вызвать сбой, по крайней мере, с отладочной сборкой. Похоже на проблему с безопасностью потоков, хотя на самом деле должен быть только один поток пользовательского интерфейса. Странный.

The incredible Jan 28.10.2020 09:45

Проверьте currentDestination перед вызовом навигации. Это может быть полезно.

Например, если у вас есть два назначения фрагмента на навигационном графе fragmentA и fragmentB, и есть только одно действие от fragmentA к fragmentB. вызов navigate(R.id.action_fragmentA_to_fragmentB) приведет к появлению IllegalArgumentException, когда вы уже были на fragmentB. Поэтому вы всегда должны проверять currentDestination перед навигацией.

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

У меня есть приложение для поиска, которое выполняет действие с аргументами. Таким образом, он мог переходить от текущего пункта назначения к самому себе. В итоге я сделал то же самое, за исключением navController.currentDestination == navController.graph.node. Это было немного грязно, и я чувствую, что не должен этого делать.

Shawn Maybush 03.01.2019 10:22

Библиотека не должна заставлять нас делать эту проверку, это действительно смешно.

DaniloDeQueiroz 05.04.2019 21:07

Я была такая же проблема. У меня был EditText и кнопка «Сохранить» для хранения содержимого EditText в базе данных. Он всегда вылетал при нажатии кнопки «сохранить». Я подозреваю, что причина кроется в том, что для того, чтобы иметь возможность нажать кнопку «Сохранить», мне нужно избавиться от экранной клавиатуры, нажав кнопку «Назад».

The Fox 17.10.2019 12:34

Это проверяет наличие ошибки, но не решает проблему. Интересно, что это условие выполняется, если задний стек навигации становится пустым по нежелательным причинам.

Mike76 25.11.2019 17:43

Как передать аргументы ??

IgorGanapolsky 27.05.2020 23:10

даже в iOS, хотя иногда несколько ViewController нажимаются, когда вы нажимаете кнопку несколько раз. Думаю, эта проблема есть и у Android, и у iOS.

coolcool1994 12.06.2020 19:55

Я делал навигацию на основе значения живых данных. Добавление этого устранило мою проблему.

CanonicalBear 17.11.2020 06:56
Вопрос: Как получить правильный идентификатор фрагмента? А: Он доступен в nav_graph.xml в теге фрагмента: <fragment android:id = "@+id/theIdOfTheFragment" .../>
Ben Butterworth 04.03.2021 12:25

как я могу использовать это решение из фрагмента внутри пейджера просмотра, если у меня нет целевого идентификатора?

Omar Beshary 15.03.2021 16:24

Вы можете проверить запрошенное действие в текущем пункте назначения контроллера навигации.

ОБНОВИТЬ добавлено использование глобальных действий для безопасной навигации.

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

Это решение не будет работать для любых действий, определенных вне списка действий currentDestination. Допустим, у вас определено глобальное действие, и вы используете это действие для навигации. Это не удастся, потому что действие не определено в списке <action> currentDestination. Добавление чека типа currentDestination?.getAction(resId) != null || currentDestination?.id != resId должно решить эту проблему, но также не может охватывать все случаи.

wchristiansen 02.10.2019 21:22

@wchristiansen, спасибо за заметки. Я обновил код с использованием глобальных действий

Alex Nuts 15.10.2019 11:33

@AlexNuts отличный ответ. Я думаю, вы можете удалить ?: graph.getAction(resId) -> currentDestination?.getAction(resId) вернет действие как для глобальных, так и для неглобальных действий (я это тестировал). Кроме того, было бы лучше, если бы вы использовали Safe Args -> лучше передавать navDirections: NavDirections, чем resId и args по отдельности.

Wess 03.03.2020 12:41

@AlexNuts Обратите внимание, что это решение не поддерживает переход к тому же месту назначения, что и текущий пункт назначения. I.o.w. переход от пункта назначения X с помощью Bundle Y к пункту назначения X с помощью Bundle Z невозможен.

Wess 03.03.2020 12:52

Это не удастся, если действие перейдет к вложенному графу. В этом случае идентификатором назначения действия будет идентификатор вложенного графа, а не его начального назначения, минуя проверку.

Nicolas 16.08.2020 23:18

Я поймал это исключение после нескольких переименований классов. Например: У меня были классы под названием FragmentA с @+is/fragment_a в навигационном графике и FragmentB с @+id/fragment_b. Затем я удалил FragmentA и переименовал FragmentB в FragmentA. Таким образом, после этого узел FragmentA все еще оставался в навигационном графе, а android:name узла FragmentB был переименован в path.to.FragmentA. У меня было два узла с одним и тем же android:name и другим android:id, и нужное мне действие было определено на узле удаленного класса.

Это также могло произойти, если у вас есть Фрагмент A с ViewPager фрагментов B И вы пытаетесь перейти от B к C

Поскольку в ViewPager фрагменты не являются адресатом A, ваш график не будет знать, что вы находитесь на B.

Решением может быть использование ADirections в B для перехода к C

В этом случае сбой происходит не каждый раз, а бывает редко. Как это решить?

Srikar Reddy 14.05.2019 16:07

Вы можете добавить глобальное действие внутри navGraph и использовать его для навигации

Abraham Mathew 05.07.2019 12:59

Поскольку B не должен знать своего точного родителя, было бы лучше использовать ADirections через интерфейс, такой как (parentFragment as? XActionListener)?.Xaction(), и обратите внимание, что вы можете сохранить эту функцию как локальную переменную, если это полезно.

hmac 17.09.2019 09:50

не могли бы вы поделиться образцом кода, чтобы проиллюстрировать это, поскольку у меня такая же проблема

Ikhiloya Imokhai 11.12.2019 14:41

кто-нибудь может, пожалуйста, образец кода, я застрял в той же проблеме. Есть фрагмент, а затем фрагмент табуляции

Usman Zafer 24.03.2020 18:25

TL; DR Оберните ваши вызовы 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) вызывается графом, и задний стек изменяет почти мгновенно, пока пользовательский интерфейс еще не обновлен. Просто недостаточно быстро, но это нормально. После смены бэк-стека навигационная система получает второй вызов navigate(R.id.my_next_destination). Поскольку задний стек изменился, теперь мы работаем относительно верхнего фрагмента в стеке. Верхний фрагмент - это фрагмент, к которому вы переходите с помощью R.id.my_next_destination, но он не содержит следующих пунктов назначения с идентификатором R.id.my_next_destination. Таким образом, вы получаете IllegalArgumentException из-за идентификатора, о котором фрагмент ничего не знает.

Эту точную ошибку можно найти в методе NavController.javafindDestination.

Для предотвращения сбоя я сделал следующее:

У меня есть BaseFragment, туда я добавил этот fun, чтобы убедиться, что destination известен currentDestination:

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

Стоит отметить, что я использую плагин SafeArgs.

Это должен быть принятый ответ. Принятый ответ не поддерживает переход к диалоговому окну

Marek Teuchner 18.02.2021 03:11

Это случилось со мной, моя проблема заключалась в том, что я нажимал FAB на tab item fragment. Я пытался перейти от одного из фрагментов вкладки к another fragment.

Но согласно Ян Лейк в этот ответ мы должны использовать tablayout и viewpager, нет поддержки компонентов навигации. Из-за этого не существует пути навигации от макета вкладки, содержащего фрагмент, к фрагменту элемента вкладки.

бывший:

containing fragment -> tab layout fragment -> tab item fragment -> another fragment

Решением было создать путь от макета вкладки, содержащей фрагмент, к намеченному фрагменту. пример: путь: container fragment -> another fragment

Недостаток:

  • График навигации больше не отображает точно пользовательский поток.

Мне это приходит в голову, когда я нажимаю кнопку назад два раза. Сначала я перехватываю 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
        }
    }

Когда это случается со мной во второй раз, и его статус такой же, как и у первого, я обнаружил, что, возможно, использую функцию adsurd. Давайте проанализируем эти ситуации.

  1. Сначала FragmentA переходит к FragmentB, затем FragmentB переходит к FragmentA, затем нажимает кнопку возврата ... появляется сбой.

  2. Во-вторых, FragmentA переходит к FragmentB, затем FragmentB переходит к FragmentC, FragmentC переходит к FragmentA, затем нажимает кнопку возврата ... появляется сбой.

Поэтому я думаю, что при нажатии кнопки «Назад» FragmentA вернется к FragmentB или FragmentC, тогда это вызовет беспорядок при входе в систему. Наконец, я обнаружил, что функцию popBackStack можно использовать для возврата, а не для навигации.

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

Пока проблема действительно решена.

В моем случае проблема возникла, когда я повторно использовал один из моих фрагментов внутри фрагмента viewpager в качестве дочернего для viewpager. Фрагмент viewpager (который был родительским фрагментом) был добавлен в навигационный xml, но действие не было добавлено в родительский фрагмент 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">

Исправлена ​​проблема, добавив действие к родительскому фрагменту окна просмотра, как показано ниже:

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>

В моем случае у меня было несколько файлов навигационного графа, и я пытался перейти от одного местоположения навигационного графа к месту назначения в другом навигационном графе.

Для этого мы должны включить 2-й граф навигации в 1-й, как это

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

на втором графике.

Подробнее здесь

Похоже, что смешивание элемента управления fragmentManager для backstack и элемента управления Navigation Architecture для backstack также может вызвать эту проблему.

Например, исходный базовый образец CameraX использовал backstack-навигацию fragmentManager, как показано ниже, и похоже, что он неправильно взаимодействовал с навигацией:

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

Если вы регистрируете «текущее место назначения» с этой версией перед переходом от основного фрагмента (в данном случае фрагмент камеры), а затем снова регистрируете его, когда вы возвращаетесь к основному фрагменту, вы можете увидеть по идентификатору в журналах, что идентификатор это не то же самое. Предположительно, навигация обновляла его при переходе к фрагменту, а fragmntManager не обновлял его снова при возвращении. Из журналов:

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

After: D/CameraXBasic: currentDest?: androidx.navigation.fragment.FragmentNavigator$Destination@9807d8f

В обновленной версии базового примера CameraX для возврата используется навигация следующим образом:

 // Handle back button press
        view.findViewById<ImageButton>(R.id.back_button).setOnClickListener {
            Navigation.findNavController(requireActivity(), R.id.fragment_container).navigateUp()
        }

Это работает правильно, и журналы показывают тот же идентификатор при возврате к основному фрагменту.

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

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

Я подозреваю, что мораль этой истории, по крайней мере, на данный момент, заключается в том, чтобы быть очень осторожным, смешивая навигацию с навигацией fragmentManager.

Это звучит правдоподобно, я буду исследовать дальше. Кто-нибудь смог проверить или подтвердить это утверждение?

Jerry Okafor 06.05.2020 13:23

@JerryOkafor - я протестировал его в приложении, над которым работал, на основе CameraX Sample, и проверил его, но было бы неплохо увидеть, видел ли это кто-то еще. Я действительно пропустил «обратную навигацию» в одном месте в том же приложении, поэтому недавно исправил ее снова.

Mick 06.05.2020 13:33

Обновленное решение @Alex Nuts

Если для определенного фрагмента нет действий и вы хотите перейти к фрагменту

fun NavController.navigateSafe(
@IdRes actionId: Int, @IdRes fragmentId: Int, args: Bundle? = null,
navOptions: NavOptions? = null, navExtras: Navigator.Extras? = null) 
{
  if (actionId != 0) {
      val action = currentDestination?.getAction(actionId) ?: graph.getAction(actionId)
      if (action != null && currentDestination?.id != action.destinationId) {
          navigate(actionId, args, navOptions, navExtras)
    }
    } else if (fragmentId != 0 && fragmentId != currentDestination?.id)
        navigate(fragmentId, args, navOptions, navExtras)
}

У меня такая же ошибка, потому что я использовал панель навигации и getSupportFragmentManager().beginTransaction().replace( ) одновременно где-то в моем коде.

Я избавился от ошибки, используя это условие (проверка, есть ли пункт назначения):

if (Navigation.findNavController(v).getCurrentDestination().getId() == R.id.your_destination_fragment_id)
Navigation.findNavController(v).navigate(R.id.your_action);

В моем случае предыдущая ошибка была вызвана, когда я нажимал на параметры панели навигации. В основном приведенный выше код скрыл ошибку, потому что где-то в моем коде я использовал навигацию с помощью getSupportFragmentManager().beginTransaction().replace( ) Условие -

 if (Navigation.findNavController(v).getCurrentDestination().getId() ==
  R.id.your_destination_fragment_id) 

никогда не был достигнут, потому что (Navigation.findNavController(v).getCurrentDestination().getId() всегда указывал на домашний фрагмент. Вы должны использовать только функции контроллера Navigation.findNavController(v).navigate(R.id.your_action) или навигационного графика для всех ваших действий по навигации.

Нелепый, но очень мощный способ: Просто назовите это:

view?.findNavController()?.navigateSafe(action)

Просто создайте это расширение:

fun NavController.navigateSafe(
    navDirections: NavDirections? = null
) {
    try {
        navDirections?.let {
            this.navigate(navDirections)
        }
    }
    catch (e:Exception)
    {
        e.printStackTrace()
    }
}

Спасибо за этот простой для понимания, но мощный пример. Не нужно возиться со слушателями кликов, и мы все равно можем использовать все API = D

baskInEminence 12.09.2020 08:19

У этой проблемы может быть много причин. В моем случае я использовал модель MVVM и наблюдал логическое значение для навигации когда логическое значение true -> перейти еще ничего не делай и это работало нормально, но здесь была одна ошибка

при нажатии кнопки возврата из целевого фрагмента я столкнулся с той же проблемой. И проблема была в логическом объекте, поскольку я забыл изменить логическое значение на false, это создало беспорядок. Я просто создал функцию в viewModel, чтобы изменить ее значение на false и вызвал его сразу после findNavController ()

Обычно, когда это случается со мной, у меня возникает проблема, описанная Чарльзом Мадером: два события навигации запускаются в одном и том же пользовательском интерфейсе, одно меняет currentDestination, а другое терпит неудачу, потому что currentDestination изменяется. Это может произойти, если вы дважды коснетесь или щелкните два представления с помощью прослушивателя щелчков, вызывающего findNavController.navigate.

Итак, чтобы решить эту проблему, вы можете использовать if-check, try-catch или, если вам интересно, есть findSafeNavController (), который выполняет эту проверку перед переходом. Он также имеет функцию проверки на ворс, чтобы вы не забыли об этой проблеме.

GitHub

Статья с подробным описанием проблемы

Я написал это расширение

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

холыш1т все работает. Сейчас тестируем

Roger 03.06.2021 05:27

Поразмыслив над советом Яна Лейка в эта ветка твиттера, я пришел к следующему подходу. Определение 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 16.08.2020 23:22

Вы не можете выполнять модульное тестирование, когда используете функции расширения здесь и там. В качестве альтернативы, когда вы используете оболочку, вы вводите шов в компонент, поэтому вы можете имитировать компоненты в соответствии с вашими потребностями и выполнять чистое модульное тестирование.

azizbekian 17.08.2020 10:24

Я создал эту функцию расширения для фрагмента:

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

Сегодня

def navigationVersion = "2.2.1"

Проблема все еще существует. Мой подход к Котлину:

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

Если вы нажмете слишком быстро, это приведет к обнулению и сбою.

Мы можем использовать RxBinding lib, чтобы помочь в этом. Вы можете добавить дроссельную заслонку и продолжительность щелчка, прежде чем он произойдет.

 RxView.clicks(view).throttleFirst(duration, TimeUnit.MILLISECONDS)
            .subscribe(__ -> {
            });

Эти статьи при регулировании скорости на Android могут помочь. Ваше здоровье!

Если вы используете recyclerview, просто добавьте перезарядку прослушивателя кликов при щелчке, а также в своем XML-файле recyclerview используйте android:splitMotionEvents = "false"

Посмотри на ответы под моими

Crazy 26.04.2020 21:26

Я решил ту же проблему, поставив галочку перед навигацией вместо стандартного кода для мгновенного нажатия кнопки управления

 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://stackoverflow.com/a/56168225/7055259

Вы можете проверить перед навигацией, является ли фрагмент, запрашивающий навигацию, по-прежнему текущим пунктом назначения, взято из этой сути.

По сути, он устанавливает тег на фрагменте для последующего поиска.

/**
 * 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 - это просто идентификатор, который вам нужно добавить в свой ids.xml, чтобы убедиться, что он уникален. <item name = "tag_navigation_destination_id" type = "id" />

Подробнее об ошибке и решении, а также о методах расширения navigateSafe(...) в «Исправление ужасного«… неизвестно этому NavController »

Я изучил несколько различных решений этой проблемы, и ваше решение определенно самое хорошее. Мне грустно видеть так мало любви к этому.

Luke 07.05.2020 19:21

может быть полезно создать уникальный идентификатор вместо NAV_DESTINATION_ID с чем-то вроде этого stackoverflow.com/a/15021758/1572848

William Reed 08.05.2020 17:16

Откуда берется тег и зачем он нужен? У меня проблемы, когда фактические идентификаторы навигационного компонента не совпадают с идентификаторами R.id.

riezebosch 25.06.2020 15:19
R.id.tag_navigation_destination_id - это просто идентификатор, который вам нужно добавить в свой ids.xml, чтобы убедиться, что он уникален. <item name = "tag_navigation_destination_id" type = "id" />
Frank 25.06.2020 15:58

Даже с этим решением возможно, что исходный сбой все еще происходит при извлечении backstack. Вы можете добавить fun Fragment.popBackStackSafe() { if (mayNavigate()) findNavController().popBackStack() }

Luke 21.09.2020 16:34

Эта ошибка могла произойти из-за того, что вы могли назначить целевой экран неправильному графику.

Каково решение?

IgorGanapolsky 12.07.2020 20:33

@IgorGanapolsky просто замените экран вашего navigation на правый график

Max Zonov 21.07.2020 19:32

Чтобы предотвратить сбой, я рекомендую использовать плагин safeargs, в противном случае вам нужно проверять каждый раз, существует идентификатор места назначения или нет. Другая вещь находится в вашем файле меню для нижней навигации, каждый идентификатор элемента должен быть таким же, как идентификатор фрагмента, который добавлен в файл xml графа навигации.

Если вы используете kotlin, вам нужно добавить некоторые зависимости в свой build.gradle.

android{
compileOptions {
        sourceCompatibility JavaVersion.VERSION_1_8
        targetCompatibility JavaVersion.VERSION_1_8
    }

    kotlinOptions {
        jvmTarget = JavaVersion.VERSION_1_8.toString()
    }
}

dependencies {
implementation 'androidx.navigation:navigation-fragment-ktx:2.2.1'
implementation 'androidx.navigation:navigation-ui-ktx:2.2.1'
}

Для SafeArgs

dependencies {
classpath  'android.arch.navigation:navigation-safe-args-gradle-plugin:1.0.0'
}

Чтобы перейти к месту назначения, сначала вы должны соединить их через действие в xml-файле графа навигации.

<fragment
        android:id = "@+id/employeesListFragment"
        android:name = "com.example.navigationjetpacksample.fragments.EmployeesListFragment"
        android:label = "EmployeesListFragment"
        tools:layout = "@layout/frag_employees_list">
        <action
            android:id = "@+id/action_employeesListFragment_to_addEmployeeFragment"
            app:destination = "@id/addEmployeeFragment" />
    </fragment>

После сборки / очистки вашего проекта,

 fab_add.setOnClickListener {   it.findNavController().navigate(EmployeesListFragmentDirections.actionEmployeesListFragmentToAddEmployeeFragment())
 }

Для дополнительной информации: Пример навигации на реактивном ранце

Я решаю эту проблему, проверяя, существует ли следующее действие в текущем пункте назначения.

public static void launchFragment(BaseFragment fragment, int action) {
    if (fragment != null && NavHostFragment.findNavController(fragment).getCurrentDestination().getAction(action) != null) {       
        NavHostFragment.findNavController(fragment).navigate(action);
    }
}

public static void launchFragment(BaseFragment fragment, NavDirections directions) {
    if (fragment != null && NavHostFragment.findNavController(fragment).getCurrentDestination().getAction(directions.getActionId()) != null) {       
        NavHostFragment.findNavController(fragment).navigate(directions);
    }
}

Это решает проблему, если пользователь быстро нажимает на две разные кнопки.

Обновите до @AlexNuts ответ, чтобы поддерживать переход к вложенному графу. Когда действие использует вложенный граф в качестве пункта назначения, например:

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

Идентификатор пункта назначения этого действия нельзя сравнивать с текущим пунктом назначения, поскольку текущий пункт назначения не может быть графиком. Должен быть разрешен начальный пункт назначения вложенного графа.

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" к действиям, для которых вы не хотите дублировать пункты назначения.

Как упоминалось в других ответах, это исключение обычно возникает, когда пользователь

  1. одновременно нажимает на несколько представлений, которые обрабатывают навигацию
  2. нажимает несколько раз на представление, которое обрабатывает навигацию.

Использование таймера для отключения щелчков не является подходящим способом решения этой проблемы. Если пользователь не был перемещен к месту назначения после истечения таймера, приложение все равно выйдет из строя, и во многих случаях, когда навигация не является действием, которое нужно выполнять, необходимы быстрые щелчки.

В случае 1 должны помочь android:splitMotionEvents = "false" в xml или setMotionEventSplittingEnabled(false) в исходном файле. Установка для этого атрибута значения false позволит только одному представлению сделать щелчок. Можете про это почитать здесь

В случае 2 что-то задерживает процесс навигации, позволяя пользователю нажимать на представление несколько раз (вызовы API, анимация и т. д.). По возможности следует устранить основную проблему, чтобы навигация происходила мгновенно, не позволяя пользователю дважды щелкнуть представление. Если задержка неизбежна, как в случае вызова API, отключение представления или отключение его щелчка будет подходящим решением.

Чтобы избежать этого сбоя, один из моих коллег написал небольшую библиотеку, которая предоставляет SafeNavController, оболочку для NavController и обрабатывает случаи, когда этот сбой происходит из-за нескольких команд навигации одновременно.

Вот короткий статья обо всей проблеме и ее решении.

Вы можете найти библиотеку здесь.

Я вызываю 2.3.1 Navigation, и такая же ошибка возникает при изменении конфигурации приложения. Когда причина проблемы была обнаружена с помощью отладки, GaphId в NavHostFragment не вступил в силу в качестве идентификатора, установленного в настоящее время при вызове navController.setGraph(). GraphId из NavHostFragment можно получить только из тега <androidx.fragment.app.FragmentContainerView/>. В настоящее время эта проблема возникает, если в вашем коде динамически установлено несколько GraphId. Когда интерфейс восстановлен, пункт назначения не может быть найден в кэшированном GraphId. Вы можете решить эту проблему, указав вручную значение mGraphId в NavHostFragment через отражение при переключении Graph.

navController.setGraph(R.navigation.home_book_navigation);
try {
    Field graphIdField = hostFragment.getClass().getDeclaredField("mGraphId");
    graphIdField.setAccessible(true);
    graphIdField.set(navHostFragment, R.navigation.home_book_navigation);
} catch (NoSuchFieldException | IllegalAccessException e) {
    e.printStackTrace();
}

Еще одно решение той же проблемы с быстрым нажатием и навигацией:

fun NavController.doIfCurrentDestination(@IdRes destination: Int, action: NavController.()-> Unit){
    if (this.currentDestination?.id == destination){action()}
}

а затем используйте вот так:

findNavController().doIfCurrentDestination(R.id.my_destination){ navigate(...) }

Преимущества этого решения заключаются в том, что вы можете легко обернуть любой существующий вызов naviagte() любой подписью, которую вы уже используете, без необходимости делать миллион перегрузок.

Попробуй это

  1. Создайте эту функцию расширения (или обычную функцию):

ОБНОВЛЕНО (без отражения и более читабельно)

import androidx.fragment.app.Fragment
import androidx.navigation.NavController
import androidx.navigation.NavDirections
import androidx.navigation.fragment.FragmentNavigator

fun Fragment.safeNavigateFromNavController(directions: NavDirections) {
    val navController = findNavController()
    val destination = navController.currentDestination as FragmentNavigator.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. И используйте это из своего фрагмента:
val direction = FragmentOneDirections.actionFragmentOneToFragmentTwo()
// new usage
safeNavigateFromNavController(direction)

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

Моя проблема была

У меня есть фрагмент (FragmentOne), который переходит к двум другим фрагментам (FragmentTwo и FragmentThree). В некоторых небольших устройствах пользователь нажимает кнопку, которая перенаправляет на FragmentTwo, но через несколько миллисекунд после того, как пользователь нажимает кнопку, которая перенаправляет на FragmentThree. Результат:

Fatal Exception: java.lang.IllegalArgumentException Navigation action/destination action_fragmentOne_to_fragmentTwo cannot be found from the current destination Destination(fragmentThree) class=FragmentThree

Мое решение было:

Проверяю, есть ли ошибка текущий пункт назначения принадлежит текущему фрагменту. Если это правда, я выполняю действие навигации.

Это все!

Это отлично работает, спасибо!

Alex Shevchyshen 07.02.2021 16:57

@AlexShevchyshen Выложил обновление без рефлексии и более читабельно.

Abner Escócio 07.02.2021 19:19

+1, но вы должны передать объект navDirections в этой функции расширения, а не в действии safeNavigateFromNavController (navDirection)

Daniyal Javaid 15.02.2021 02:12

Этот ответ следует принять.

Abhishek Singh 04.06.2021 00:13

Ответил по ссылке: https://stackoverflow.com/a/67614469/5151336

Добавлена ​​функция расширения для навигатора для безопасной навигации.

Другие вопросы по теме