У меня проблема с новым компонентом архитектуры навигации Android, когда я пытаюсь перейти с от одного фрагмента к другому, я получаю эту странную ошибку:
java.lang.IllegalArgumentException: navigation destination XXX
is unknown to this NavController
Любая другая навигация работает нормально, кроме этой.
Я использую функцию фрагмента findNavController(), чтобы получить доступ к NavController.
Любая помощь будет оценена по достоинству.
До сих пор частота появления этой ошибки была снижена с новыми выпусками библиотеки, но я думаю, что библиотека еще недостаточно хорошо документирована.
В моем случае ошибка возникла из-за того, что у меня было действие навигации с включенными опциями Single Top и Clear Task после экрана-заставки.
Но clearTask устарел, вместо него следует использовать popUpTo ().
@ Po10cio Ни один из этих флагов не понадобился, я просто удалил его, и он был исправлен.
Похоже, вы решаете задачу. Приложение может иметь одноразовую настройку или серию экранов входа в систему. Эти условные экраны не следует рассматривать как начальную точку назначения вашего приложения.
https://developer.android.com/topic/libraries/architecture/navigation/navigation-conditional
В моем случае я использовал настраиваемую кнопку возврата для перехода вверх. Я позвонил onBackPressed() вместо следующего кода
findNavController(R.id.navigation_host_fragment).navigateUp()
Это вызвало появление IllegalArgumentException. После того, как я изменил его на использование метода navigateUp(), у меня больше не было сбоев.
Я не понимаю, в чем разница между onBackPressed и этим, все еще застрял на кнопке возврата системы и переопределил ее и заменил это кажется сумасшедшим
Я согласен, это кажется безумным. Многие вещи, с которыми я столкнулся в компоненте архитектуры навигации Android, кажутся немного сумасшедшими, они слишком жестко настроены IMO. Думаю о собственной реализации для нашего проекта, потому что это создает слишком много головной боли
У меня не работает ... По-прежнему возникает та же ошибка.
В моем случае, если пользователь дважды очень быстро щелкнет одно и то же представление, произойдет сбой. Поэтому вам нужно реализовать какую-то логику, чтобы предотвратить множественные быстрые щелчки ... Что очень раздражает, но, похоже, необходимо.
Вы можете прочитать больше о том, как предотвратить это здесь: Android: предотвращение двойного щелчка по кнопке
Изменить 19 марта 2019 г.: Чтобы уточнить немного больше, этот сбой не может быть воспроизведен исключительно простым «очень быстрым щелчком по одному и тому же виду». В качестве альтернативы вы можете просто использовать два пальца и одновременно щелкнуть два (или более) представления, при этом каждое представление имеет свою собственную навигацию, которую они будут выполнять. Это легко сделать с помощью особенно, когда у вас есть список элементов. Вышеупомянутая информация о предотвращении множественных кликов поможет в этом случае.
Изменить 16.04.2020: На всякий случай, если вам не очень интересно читать этот пост о переполнении стека выше, я включаю свое собственное (Kotlin) решение, которое я использую уже давно.
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
}
}
fun View.setOnSingleClickListener(l: View.OnClickListener) {
setOnClickListener(OnSingleClickListener(l))
}
fun View.setOnSingleClickListener(l: (View) -> Unit) {
setOnClickListener(OnSingleClickListener(l))
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
settingsButton.setOnSingleClickListener {
// navigation call here
}
}
Редактирование об использовании 2-х пальцев и одновременном нажатии 2-х представлений! Это ключ для меня и помог мне легко воспроизвести проблему. Отличное обновление с этой информацией.
Во время фазы отладки я случайно щелкнул, когда приложение зависло в ожидании продолжения выполнения. Похоже на еще один случай двух последовательных нажатий на IDE
Спасибо за это. Спас меня от нескольких вылетов и головной боли :)
Это решение - хитрость, позволяющая обойти настоящую проблему: компонент навигации. Он также склонен к сбою на более медленных устройствах. Создание и наполнение нового фрагмента может занять более 200 мс. После этой задержки может быть отправлено второе событие щелчка, прежде чем фрагмент будет показан, и мы вернемся к той же проблеме.
На самом деле не работает в моем приложении. Я все еще могу щелкать достаточно быстро, чтобы вызвать сбой, по крайней мере, с отладочной сборкой. Похоже на проблему с безопасностью потоков, хотя на самом деле должен быть только один поток пользовательского интерфейса. Странный.
Проверьте 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. Это было немного грязно, и я чувствую, что не должен этого делать.
Библиотека не должна заставлять нас делать эту проверку, это действительно смешно.
Я была такая же проблема. У меня был EditText и кнопка «Сохранить» для хранения содержимого EditText в базе данных. Он всегда вылетал при нажатии кнопки «сохранить». Я подозреваю, что причина кроется в том, что для того, чтобы иметь возможность нажать кнопку «Сохранить», мне нужно избавиться от экранной клавиатуры, нажав кнопку «Назад».
Это проверяет наличие ошибки, но не решает проблему. Интересно, что это условие выполняется, если задний стек навигации становится пустым по нежелательным причинам.
Как передать аргументы ??
даже в iOS, хотя иногда несколько ViewController нажимаются, когда вы нажимаете кнопку несколько раз. Думаю, эта проблема есть и у Android, и у iOS.
Я делал навигацию на основе значения живых данных. Добавление этого устранило мою проблему.
<fragment android:id = "@+id/theIdOfTheFragment" .../>как я могу использовать это решение из фрагмента внутри пейджера просмотра, если у меня нет целевого идентификатора?
Вы можете проверить запрошенное действие в текущем пункте назначения контроллера навигации.
ОБНОВИТЬ добавлено использование глобальных действий для безопасной навигации.
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, спасибо за заметки. Я обновил код с использованием глобальных действий
@AlexNuts отличный ответ. Я думаю, вы можете удалить ?: graph.getAction(resId) -> currentDestination?.getAction(resId) вернет действие как для глобальных, так и для неглобальных действий (я это тестировал). Кроме того, было бы лучше, если бы вы использовали Safe Args -> лучше передавать navDirections: NavDirections, чем resId и args по отдельности.
@AlexNuts Обратите внимание, что это решение не поддерживает переход к тому же месту назначения, что и текущий пункт назначения. I.o.w. переход от пункта назначения X с помощью Bundle Y к пункту назначения X с помощью Bundle Z невозможен.
Это не удастся, если действие перейдет к вложенному графу. В этом случае идентификатором назначения действия будет идентификатор вложенного графа, а не его начального назначения, минуя проверку.
Я поймал это исключение после нескольких переименований классов. Например:
У меня были классы под названием 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
В этом случае сбой происходит не каждый раз, а бывает редко. Как это решить?
Вы можете добавить глобальное действие внутри navGraph и использовать его для навигации
Поскольку B не должен знать своего точного родителя, было бы лучше использовать ADirections через интерфейс, такой как (parentFragment as? XActionListener)?.Xaction(), и обратите внимание, что вы можете сохранить эту функцию как локальную переменную, если это полезно.
не могли бы вы поделиться образцом кода, чтобы проиллюстрировать это, поскольку у меня такая же проблема
кто-нибудь может, пожалуйста, образец кода, я застрял в той же проблеме. Есть фрагмент, а затем фрагмент табуляции
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);
}
Способ легко воспроизвести ошибку - нажать несколькими пальцами на список элементов, где щелчок по каждому элементу разрешается при переходе к новому экрану (в основном так же, как отмечали люди - два или более щелчка за очень короткий период времени ). Я заметил, что:
navigate всегда работает нормально;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.
Это должен быть принятый ответ. Принятый ответ не поддерживает переход к диалоговому окну
Это случилось со мной, моя проблема заключалась в том, что я нажимал 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. Давайте проанализируем эти ситуации.
Сначала 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
)
Пока проблема действительно решена.
В моем случае проблема возникла, когда я повторно использовал один из моих фрагментов внутри фрагмента 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.
Это звучит правдоподобно, я буду исследовать дальше. Кто-нибудь смог проверить или подтвердить это утверждение?
@JerryOkafor - я протестировал его в приложении, над которым работал, на основе CameraX Sample, и проверил его, но было бы неплохо увидеть, видел ли это кто-то еще. Я действительно пропустил «обратную навигацию» в одном месте в том же приложении, поэтому недавно исправил ее снова.
Обновленное решение @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
У этой проблемы может быть много причин. В моем случае я использовал модель MVVM и наблюдал логическое значение для навигации когда логическое значение true -> перейти еще ничего не делай и это работало нормально, но здесь была одна ошибка
при нажатии кнопки возврата из целевого фрагмента я столкнулся с той же проблемой. И проблема была в логическом объекте, поскольку я забыл изменить логическое значение на false, это создало беспорядок. Я просто создал функцию в viewModel, чтобы изменить ее значение на false и вызвал его сразу после findNavController ()
Обычно, когда это случается со мной, у меня возникает проблема, описанная Чарльзом Мадером: два события навигации запускаются в одном и том же пользовательском интерфейсе, одно меняет currentDestination, а другое терпит неудачу, потому что currentDestination изменяется. Это может произойти, если вы дважды коснетесь или щелкните два представления с помощью прослушивателя щелчков, вызывающего findNavController.navigate.
Итак, чтобы решить эту проблему, вы можете использовать if-check, try-catch или, если вам интересно, есть findSafeNavController (), который выполняет эту проверку перед переходом. Он также имеет функцию проверки на ворс, чтобы вы не забыли об этой проблеме.
Я написал это расширение
fun Fragment.navigateAction(action: NavDirections) {
val navController = this.findNavController()
if (navController.currentDestination?.getAction(action.actionId) == null) {
return
} else {
navController.navigate(action)
}
}
холыш1т все работает. Сейчас тестируем
Поразмыслив над советом Яна Лейка в эта ветка твиттера, я пришел к следующему подходу. Определение 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 есть функции расширения именно для этой цели, без оболочки.
Вы не можете выполнять модульное тестирование, когда используете функции расширения здесь и там. В качестве альтернативы, когда вы используете оболочку, вы вводите шов в компонент, поэтому вы можете имитировать компоненты в соответствии с вашими потребностями и выполнять чистое модульное тестирование.
Я создал эту функцию расширения для фрагмента:
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"
Посмотри на ответы под моими
Я решил ту же проблему, поставив галочку перед навигацией вместо стандартного кода для мгновенного нажатия кнопки управления
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 */
согласно этому ответу
Вы можете проверить перед навигацией, является ли фрагмент, запрашивающий навигацию, по-прежнему текущим пунктом назначения, взято из этой сути.
По сути, он устанавливает тег на фрагменте для последующего поиска.
/**
* 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 »
Я изучил несколько различных решений этой проблемы, и ваше решение определенно самое хорошее. Мне грустно видеть так мало любви к этому.
может быть полезно создать уникальный идентификатор вместо NAV_DESTINATION_ID с чем-то вроде этого stackoverflow.com/a/15021758/1572848
Откуда берется тег и зачем он нужен? У меня проблемы, когда фактические идентификаторы навигационного компонента не совпадают с идентификаторами R.id.
R.id.tag_navigation_destination_id - это просто идентификатор, который вам нужно добавить в свой ids.xml, чтобы убедиться, что он уникален. <item name = "tag_navigation_destination_id" type = "id" />Даже с этим решением возможно, что исходный сбой все еще происходит при извлечении backstack. Вы можете добавить fun Fragment.popBackStackSafe() { if (mayNavigate()) findNavController().popBackStack() }
Эта ошибка могла произойти из-за того, что вы могли назначить целевой экран неправильному графику.
Каково решение?
@IgorGanapolsky просто замените экран вашего navigation на правый график
Чтобы предотвратить сбой, я рекомендую использовать плагин 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 должны помочь 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() любой подписью, которую вы уже используете, без необходимости делать миллион перегрузок.
Попробуй это
ОБНОВЛЕНО (без отражения и более читабельно)
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)
}
}
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
Мое решение было:
Проверяю, есть ли ошибка текущий пункт назначения принадлежит текущему фрагменту. Если это правда, я выполняю действие навигации.
Это все!
Это отлично работает, спасибо!
@AlexShevchyshen Выложил обновление без рефлексии и более читабельно.
+1, но вы должны передать объект navDirections в этой функции расширения, а не в действии safeNavigateFromNavController (navDirection)
Этот ответ следует принять.
Ответил по ссылке: https://stackoverflow.com/a/67614469/5151336
Добавлена функция расширения для навигатора для безопасной навигации.
Пожалуйста, предоставьте код для лучшего понимания.