#android #kotlin #android-architecture-navigation
#Android #kotlin #android-архитектура-навигация
Вопрос:
У меня есть фрагмент A, B, C. Все в порядке при навигации из A -> B, но из B -> C происходит сбой.
Вот моя навигация
Вот мой навигационный код
categoryProductItemListAdapter.setOnItemClickListener {
val action = CategoryProductItemsDirections.actionCategoryProductItems2ToProductItem(null, it)
navController = Navigation.findNavController(requireView())
navController?.navigateUp()
navController?.navigate(action)
}
Вот XML-код для назначения в ProductItem
<fragment
android:id="@ id/categoryProductItems2"
android:name="com.sample.store.main.dashboard.ui.ui.home.categoryitems.CategoryProductItems"
android:label="CategoryProductItems"
tools:layout="@layout/fragment_category_product_items">
<argument
android:name="category_global"
app:argType="com.sample.store.data.globalmodels.response.categories.Category" />
<action
android:id="@ id/action_categoryProductItems2_to_productItem"
app:destination="@id/productItem"
app:enterAnim="@anim/enter_from_right"
app:exitAnim="@anim/exit_to_right"
app:popEnterAnim="@anim/fragment_open_enter"
app:popExitAnim="@anim/fragment_fade_exit" />
</fragment>
И вот ошибка:
java.lang.IllegalArgumentException: Navigation action/destination com.sample.store.full:id/action_categoryProductItems2_to_productItem cannot be found from the current destination Destination(id/navigation_home) label=Home class=com.sample.store.main.dashboard.ui.ui.home.mainui.HomeFragment
Я не знаю, что произошло, но кажется, что NavController ищет «navigation_home»
Комментарии:
1. можете ли вы поделиться полным nav_host.xml файл
2. @Cyd удаление этой строки `NavController?.navigateUp ()` может решить вашу проблему
Ответ №1:
Это скорее предупреждение, чем ответ. Но я надеюсь, что это поможет.
Резюме: (Как уже говорили другие:) Причиной большинства этих исключений являются последовательные вызовы функций навигации.
Учитывая, как структурированы компоненты Android, особенно как работает MediatorLiveData, люди иногда могут захотеть объединить узлы данных в одном наблюдаемом держателе данных (LiveData).
Если наблюдение за этим посредником связано с функциями динамической навигации, несомненно, появятся ошибки.
Причина в том, что источники могут изменять значение LiveData последовательное число раз, равное количеству источников, подключенных к посреднику.
Это совершенно хорошая идея, НО. Повторные изменения в NavController определенно приведут к нежелательным результатам.
Это может включать:
-
дважды выскакивает backStack.
-
Переход из A -> B дважды подряд выдает исключение «не найден
» во второй раз.
Это большая проблема с тестированием, особенно потому, что проблема с одним фрагментом может каскадно распространяться на нижележащие стеки, и поэтому, когда в одном фрагменте может возникнуть исключение из не найденного направления, реальный виновник может быть найден во фрагменте поверх того, который выдает исключение.
На самом деле это было бы легко решить, создав самоустанавливающийся исполнитель потока scheduled.cancel(true);
с допуском задержки для самих MediatorLiveData (точнее, onChange, а не setValue(), поскольку нетерпеливые обновления внутреннего состояния — это вся и единственная цель / шутка посредника IMHO (извините, postValue() не допускается!)).
Не говоря уже о том, что сам посредник является неполным компонентом…
Другой более простой подход заключается в том, чтобы убедиться, что вызовы onChange из MutableLiveData выполняются тогда и только тогда, когда!Объект::Equals и предотвращает повторные вызовы onChange(), что по-прежнему свидетельствует о неполноте MediatorLiveData / LiveData. (Просто будьте предельно осторожны со списками)
Любой ценой избегайте выполнения последовательных вызовов NavController, и если вы каким-то образом ДОЛЖНЫ, то отложенный запуск может быть вашим единственным способом добиться этого.
Комментарии:
1. спасибо, что указали на это. я прослушивал поток изменений навигации и подписывался на него несколько раз, когда я начал прослушивать фрагменты onViewCreated (который вызывается несколько раз)
Ответ №2:
Во-первых, вы не должны проходить requireView()
при попытке получить свой навигационный контроллер — navController = Navigation.findNavController(requireView())
. Вы должны передавать фактический экземпляр фрагмента узла навигации.
Во-вторых, проблема возникает из-за того, что вы пытаетесь вызвать навигационный путь из B -> C, когда на фрагменте A.
Ваш путь направления от B -> C
val action = CategoryProductItemsDirections.actionCategoryProductItems2ToProductItem(null, it)
Но сначала вы переходите вверх, так что теперь вы фактически находитесь на фрагменте A при попытке выполнить навигацию:
navController?.navigateUp()
navController?.navigate(action)
Комментарии:
1. При работе с вложенными фрагментами, такими как ViewPager, вы можете легко совершить ошибку, пытаясь перейти с вложенной страницы на другой фрагмент, когда на самом деле навигация должна осуществляться от фрагмента ViewPager к фрагменту назначения.
2. Интересный комментарий, купите не уверен, какое отношение имеет view pager к этому сообщению / вопросу? Нет упоминания об использовании view pager
3. Ваш ответ помог мне решить проблему, о которой я упоминал в комментарии. То есть попытка перейти из пункта назначения, в котором вы в данный момент не находитесь.
Ответ №3:
Я создал функцию расширения, чтобы проверить возможность запуска действия из текущего пункта назначения.
fun NavController.navigateSafe(@IdRes resId: Int, args: Bundle? = null) {
val destinationId = currentDestination?.getAction(resId)?.destinationId.orEmpty()
currentDestination?.let { node ->
val currentNode = when (node) {
is NavGraph -> node
else -> node.parent
}
if (destinationId != 0) {
currentNode?.findNode(destinationId)?.let { navigate(resId, args) }
}
}}
И orEmpty()
часть является расширением Int?
следующим образом:
fun Int?.orEmpty(default: Int = 0): Int {
return this ?: default
}
Комментарии:
1. значение destinationId = currentDestination? .getAction(resId)? .destinationId.orEmpty() В этой строке последний .orEmpty() является красным. я не знаю почему. if (destinationId != EMPTY_INT) { currentNode? .findNode(destinationId)? .пусть { navigate(resId, args) } } и в этом условии EMPTY_INT будет красным, значение которого будет там проверяться. направьте меня
2. @SyedRafaqatHussain выглядит так. orEmpty() — это некоторое расширение, которое применяет значение int к destinationId. Вы можете использовать destinationId как свойство с нулевым значением без этого вызова расширения и просто изменить проверку на if (destinationId != null)
3. очень хорошее решение.
4. Но когда вы передаете resId назначения вместо resId действия, эта логика не работает, потому что
currentDestination?.getAction(resId)
возвращает null.
Ответ №4:
Вот Java-версия класса NavigationUtils для безопасной навигации:
public abstract class NavigationUtils {
/**
* This function will check navigation safety before starting navigation using direction
*
* @param navController NavController instance
* @param direction navigation operation
*/
public static void navigateSafe(NavController navController, NavDirections direction) {
NavDestination currentDestination = navController.getCurrentDestination();
if (currentDestination != null) {
NavAction navAction = currentDestination.getAction(direction.getActionId());
if (navAction != null) {
int destinationId = orEmpty(navAction.getDestinationId());
NavGraph currentNode;
if (currentDestination instanceof NavGraph)
currentNode = (NavGraph) currentDestination;
else
currentNode = currentDestination.getParent();
if (destinationId != 0 amp;amp; currentNode != null amp;amp; currentNode.findNode(destinationId) != null) {
navController.navigate(direction);
}
}
}
}
/**
* This function will check navigation safety before starting navigation using resId and args bundle
*
* @param navController NavController instance
* @param resId destination resource id
* @param args bundle args
*/
public static void navigateSafe(NavController navController, @IdRes int resId, Bundle args) {
NavDestination currentDestination = navController.getCurrentDestination();
if (currentDestination != null) {
NavAction navAction = currentDestination.getAction(resId);
if (navAction != null) {
int destinationId = orEmpty(navAction.getDestinationId());
NavGraph currentNode;
if (currentDestination instanceof NavGraph)
currentNode = (NavGraph) currentDestination;
else
currentNode = currentDestination.getParent();
if (destinationId != 0 amp;amp; currentNode != null amp;amp; currentNode.findNode(destinationId) != null) {
navController.navigate(resId, args);
}
}
}
}
private static int orEmpty(Integer value) {
return value == null ? 0 : value;
}
}
Вы можете использовать этот класс следующим образом:
NavController navController = Navigation.findNavController(view);
NavigationUtils.navigateSafe(navController, R.id.action_firstFragment_to_secondFragment, null);
или:
NavController navController = Navigation.findNavController(view);
NavDirections direction = FirstFragmentDirections.actionFirstFragmentToSecondFragment(yourModel, bundleArgs);
NavigationUtils.navigateSafe(navController, direction);
Комментарии:
1. У меня это сработало с небольшими изменениями в моей кодовой базе kotlin, но основная идея та же. Это действительно помогло мне на самом деле.
Ответ №5:
// Проверьте, является ли текущий фрагмент фрагментом, вызванным событием, передав идентификатор
fun Fragment.findNavControllerSafely(id: Int): NavController? {
return if (findNavController().currentDestination?.id == id) {
findNavController()
} else {
null
}
}
// Реализовать во фрагменте, где вы вызываете навигацию
findNavControllerSafely(R.id.fragment1)?.navigate(
R.id.action_fragment1_to_fragment2, bundle
)
Комментарии:
1. Это работает, @Gobinath Nataraj
Ответ №6:
Такого рода ошибки появляются в основном в списке элементов, и нажатие на элемент запускает навигацию.
Я решил с помощью этого кода, при щелчке по элементу перед вызовом функции навигации я проверю, является ли текущий пункт назначения предполагаемым, поскольку,
val currentDestinationIsHome = this.findNavController().currentDestination == this.findNavController().findDestination(R.id.nav_home)
val currentDestinationIsDetail = this.findNavController().currentDestination == this.findNavController().findDestination(R.id.nav_detail)
if(currentDestinationIsHome amp;amp; !currentDestinationIsDetail){
....
// perform navigation
}
Это гарантирует, что навигация выполняется только тогда, когда пункты назначения находятся в законном состоянии. [Нет исключения IllegalStateException … :)) ]
Ответ №7:
Функция расширения Kotlin для безопасной навигации путем передачи действия NavDirections с использованием Java-метода @Homayoon Ahmadi и функции расширения @vishnu benny’s orEmpty().
fun NavController.navigateSafely(direction: NavDirections) {
val currentDestination = this.currentDestination
if (currentDestination != null) {
val navAction = currentDestination.getAction(direction.actionId)
if (navAction != null) {
val destinationId: Int = navAction.destinationId.orEmpty()
val currentNode: NavGraph? = if (currentDestination is NavGraph) currentDestination else currentDestination.parent
if (destinationId != 0 amp;amp; currentNode != null amp;amp; currentNode.findNode(destinationId) != null) {
this.navigate(direction)
}
}
}
}
fun Int?.orEmpty(default: Int = 0): Int {
return this ?: default
}
Ответ №8:
В моем случае я решаю проблему, заменяя —
<action
android:id="@ id/action_categoryProductItems2_to_productItem"
app:destination="@id/productItem"
app:enterAnim="@anim/enter_from_right"
app:exitAnim="@anim/exit_to_right"
app:popEnterAnim="@anim/fragment_open_enter"
app:popExitAnim="@anim/fragment_fade_exit"/>
с
<action
android:id="@ id/action_categoryProductItems2_to_productItem"
app:destination="@id/productItem"
app:enterAnim="@anim/enter_from_right"
app:exitAnim="@anim/exit_to_right"
app:popEnterAnim="@anim/fragment_open_enter"
app:popExitAnim="@anim/fragment_fade_exit"
app:popUpToInclusive="true" /* If true then also remove the destination from stack while popup */
app:popUpTo="@id/navigation_home"/> /*The fragment where to land again from destination*/
Ответ №9:
В моем случае я решаю проблему, заменяя
implementation "android.arch.navigation:navigation-fragment-ktx:1.0.0"
с
implementation "androidx.navigation:navigation-fragment-ktx:2.3.5"
Ответ №10:
К сожалению, решения, основанные на NavigationUtils, которые используют только метод findNode() класса NavGraph, имеют один серьезный недостаток — невозможно перейти к месту назначения, которое указывает на сам текущий NavGraph. Другими словами, метод findNode() ничего не найдет.
В качестве примера можно рассмотреть действие action_startingFragment_to_startingFragment на графике ниже:
<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/some_graph"
app:startDestination="@id/startingFragment">
<fragment
android:id="@ id/startingFragment"
android:name="com.xxx.StartingFragment">
<action
android:id="@ id/action_startingFragment_to_startingFragment"
app:destination="@id/some_graph"
app:popUpTo="@id/startingFragment"
app:popUpToInclusive="true" />
</fragment>
</navigation>
Чтобы учесть упомянутый случай, также необходимо проверить, является ли сам найденный текущий узел целевым пунктом назначения или нет.
Таким образом, функция расширения будет выглядеть следующим образом:
fun NavController.navigateSafe(@IdRes actionId: Int, args: Bundle?) {
currentDestination?.let { currentDestination ->
val navAction = currentDestination.getAction(actionId)
// to navigate successfully certain action should be explicitly stated in nav graph
if (navAction != null) {
val destinationId = navAction.destinationId
if (destinationId != 0) {
val currentNode = currentDestination as? NavGraph ?: currentDestination.parent
if (currentNode?.id == destinationId || <--------- THIS CONDITION IS THE KEY
currentNode?.findNode(destinationId) != null
) {
navigate(actionId, args, null)
}
}
}
}
}
Обновить:
Также отсутствует более интересный случай, когда пункт назначения указывает на сам родительский NavGraph, как показано ниже.
Родительский граф:
<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/main_graph"
app:startDestination="@id/mainFragment">
<fragment
android:id="@ id/mainFragment"
android:name="com.xxx.main.MainFragment">
<action
android:id="@ id/action_main_to_some"
app:destination="@id/some_graph"/>
</fragment>
<include app:graph="@navigation/some_graph" />
</navigation>
Дочерний граф:
<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/some_graph"
app:startDestination="@id/someFragment">
<fragment
android:id="@ id/someFragment"
android:name="com.xxx.some.SomeFragment">
<action
android:id="@ id/action_some_to_main"
app:destination="@id/main_graph"
app:popUpTo="@id/someFragment"
app:popUpToInclusive="true" />
</fragment>
</navigation>
Действие action_some_to_main является целевым.
Следовательно, функция расширения должна быть изменена:
fun NavController.navigateSafe(@IdRes actionId: Int, args: Bundle?) {
currentDestination?.let { currentDestination ->
val navAction = currentDestination.getAction(actionId)
// to navigate successfully certain action should be explicitly stated in nav graph
if (navAction != null) {
val destinationId = navAction.destinationId
if (destinationId != 0) {
val currentNode = currentDestination as? NavGraph ?: currentDestination.parent
if (currentNode?.findDestination(destinationId) != null) { <----- CHANGED HERE
navigate(actionId, args, null)
}
}
}
}
}
private fun NavGraph.findDestination(destinationId: Int): NavDestination? {
if (id == destinationId) return this
val node = findNode(destinationId)
if (node != null) return node
return parent?.findDestination(destinationId)
}
Ответ №11:
В моем случае я просто изменил lifecycleScope
на viewLifecycleOwner.lifecycleScope
, потому что я зарегистрировал observer в области действия / фрагмента вместо регистрации observer в области одного фрагмента. Это предотвращает запуск навигации два или более раз. Теперь вы можете безопасно использовать:
viewLifecycleOwner.lifecycleScope.launch {
whenStarted {
viewModel.someState.collect { state ->
when (state) {
SomeState.SUCCESS -> {
val action = ...
findNavController().navigate(action)
}
}
}
}
}
Ответ №12:
Вам нужно установить defaultNavHost=»true». Как в этом примере:
<LinearLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical">
<androidx.fragment.app.FragmentContainerView
android:id="@ id/myNavHostFragment"
android:name="androidx.navigation.fragment.NavHostFragment"
android:layout_width="match_parent"
android:layout_height="match_parent"
app:defaultNavHost="true"
app:navGraph="@navigation/navigation_layout" />
</LinearLayout>
Также не забудьте установить домашнюю активность в вашем навигационном компоненте.