- Nested Navigation Graph
- Navigation: Nested graphs and include tag
- Intro
- Nested Navigation Graphs
- Include tag
- Summary
- Navigation Component-дзюцу, vol. 2 – вложенные графы навигации
- Навигация во вложенный граф и обратно
- Возврат результата из вложенного флоу
- Выводы по работе с вложенным флоу
- Навигация из вложенного графа во внешний граф
- Ищем «правильный» NavController
- Проблемы с навигацией по кнопке Back
- Подведём итоги
- Навигация по условию
- Выводы по кейсам вложенной навигации
Nested Navigation Graph
Jan 26, 2020 · 3 min read
Android Jetpack’s Navigation component helps you implement navigation which refers to the interactions that allow users to navigate across, into, and back out from the different pieces of content within your app.
Of course, you could have your app module depend on all of the featu r e modules and just handle the navigation within a single navigation resource file. However, thinking about bigger apps, this can become a problem really quickly. Just imagine an app that has 20 or 30 screens, how are we going to keep an overview of the screens, what is connected with what and many other problems.
So, the solution to these problems is to take benefit of Nested Graph concepts.
A series of destinations can be grouped into a nested graph within a parent navigation graph called the root graph. Nested graphs are useful to organize, optimize and reuse sections of your app’s UI.
So theoretically, we can create multiple navigation graph files (one per feature module) which only represents the module internal navigation. The navigation between the modules themselves can happen by including the graphs of the feature module to the entry point of the parent module.
Suppose you have a project with three modules — A, B and C.
Each module has its own flow like module A contains — A1, A2 screen, module B contains — B1, B2 screen and module C contain — C1, C2 screen.
So we will create a navigation graph for each module i.e for module A, module B, and module C and just use tag to connect required nav graph to root navigation_graph.xml.
Just do it inside the tag.
include app:graph=”@navigation/B_nav_graph”/>
include app:graph=”@navigation/C_nav_graph”/>
Have a look to root A_nav_graph.xml file —
And our B_nav_graph.xml file looks like —
And C_nav_graph.xml file looks like —
Note:- We can call ids of parent fragment to child fragment but not vice-versa, we can only communicate to child fragment through its start destination only.
With this little trick, we can separate our navigation graphs from each other, access them and also reuse them in the future as well.
Источник
Navigation: Nested graphs and include tag
Here we are with the third article of second Navigation series. If you prefer to watch this content instead of reading, check out the video below:
Intro
In the previous articles in this series, we added coffee tracker functionality, enhanced the user experience with navigation UI and implemented conditional navigation.
This time we’ll see how to organize the navigation graph by using nested graphs and using the include tag to import other graphs. This will allow us to modularize the app and see how navigation works with modules.
Ok time to fire up Android Studio and see how to use navigation with modules.
Nested Navigation Graphs
Let’s start with the navigation graph. Nested graphs can help you group a series of destinations within a parent navigation graph.
Looking at the navigation graph, the coffeeList and coffeeEntryDialog destinations are good candidates to convert into a nested graph. To do that, I select both fragments by holding shift and select move to nested graph.
Now, switching to code view, you can see that a nested graph is simply a new navigation graph inside the root graph.
Navigation between the selected fragments is moved into the nested graph.
Nested graphs must have an id. You can use this id to create actions to navigate to the nested graph but not directly to its child destinations. Nested graphs have their own start destination and don’t expose their child destinations separately.
If you double click on the nested graph then you can see the nested destinations and actions between them.
Include tag
Instead of using nested graphs, I can also extract the graph into a new navigation xml file. I create a new xml file called coffee_graph and move the contents of the nested graph into this new file.
I can nest this new graph in the other one using the include tag. Using the include tag offers the same functionality as nested graphs but lets you use graphs from other modules or libraries.
Similar to nested graphs, included graphs don’t expose the list of destinations in the included graph which means I need to update the menu ids which refer to coffeeList.
I update the menus to use the included graph’s id. Since CoffeeList is the startDestination of the included graph, I can use the graph id to navigate to this graph. If you try the app now, everything should be working as before.
Now that the navigation graph for coffee tracking is separate this is a good time to modularize the app and see how navigation works with modules.
If you want to follow along you can check out this repo which has all the changes I did so far.
I create 2 new modules: core and coffee. I move all common classes such as Donut, Coffee, DAOs, Database and other common resources to the core module.
Next, I move all fragments, viewModels and adapter classes used in coffee tracking into the coffee module. Layouts and other resources used in coffee tracking are also moved here, as well as the coffee_graph.
Coffee module depends on core module.
Finally, in the app module, add coffee and core modules as a dependency.
Note that nothing changes in the navigation graph; it is not affected from these changes
Now if I run the app everything works as it used to but using modules instead! You can check out the final code here.
With this change I separated the coffee tracker module and it’s navigation flow from the rest of the app which means the coffee tracker module can be used independently from the donut tracker.
Summary
In this article we’ve seen how to create nested graphs and how to use the include tag to modularize the donut tracker app.
Next article, we’ll take this further and learn how to use navigation with feature modules. Stay tuned!
Источник
Navigation Component-дзюцу, vol. 2 – вложенные графы навигации
Каждое большое приложение содержит множество способов навигации между экранами. А хорошая библиотека навигации должна помогать разработчику их реализовывать. Именно с такой мыслью я подошёл к исследованию кейсов со вложенными графами навигации.
Это вторая из трёх статей про реализацию кейсов навигации при помощи Navigation Component-а.
UPDATE 30.12.2020:
Некоторые выводы, сделанные в этой статье, некорректны, так как при настройке BottomNavigationView из прошлой статьи была допущена серьёзная ошибка, которая повлекла за собой все описанные в статье проблемы. Подробности — в последней статье цикла.
Первая статья про BottomNavigationView, третья — про многомодульность, диплинки и другие кейсы.
Навигация во вложенный граф и обратно
В этом кейсе мы говорим про следующую часть общей схемы навигации:
Представим такую ситуацию: у нас есть 4 экрана — A, B, C и D. Пусть с экранов A и B вы можете перейти на экран C, с экрана C в экран D, а после D — вернуться на тот экран, который начал флоу C->D.
В тестовом приложении, которое я приготовил для разбора Navigation Component-а, есть две вкладки BottomNavigationView (на схеме это Search и Responses – но пусть они будут экранами A и B):
С обеих этих вкладок мы можем перейти на некоторый вложенный флоу, который состоит из двух экранов (C и D):
Если мы перейдём на экран C с вкладки Search (экрана A), то после экрана D мы должны вернуться на вкладку Search:
А если мы стартуем экран C со вкладки Responses, то после завершения внутреннего флоу C->D мы должны вернуться на вкладку Responses:
Этот кейс описывает старт последовательности экранов из разных мест приложения, а после её завершения возврат на тот экран, который начал эту последовательность.
Как это реализовать? Для начала вы объявляете вашу «вложенную» последовательность экранов в отдельном XML-файле навигации, чтобы можно было вкладывать её в другие графы:
Затем следует вложить созданный граф навигации в уже существующий граф и использовать идентификатор вложенного графа для описания action-ов:
Итак, вы описали навигацию из двух разных мест приложения во вложенный флоу. Но что делать с возвратом?
Проблема в том, что Navigation Component не позволяет нормально описывать навигацию НАЗАД, только навигацию ВПЕРЁД. Но при этом даёт возможность описывать удаление экранов из back stack-а при помощи атрибутов popBackUp и popBackUpInclusive в XML, а также при помощи функции popBackStack в NavController -е.
Пока размышлял над этим, заметил интересную вещь: я подключился дебаггером перед переходом с экрана Splash на экран с нижней навигацией и обнаружил, что поле mBackStack внутри NavController -а Splash-экрана содержит два объекта NavBackStackEntry .
Честно говоря, я не ожидал увидеть там два объекта, поскольку в back stack-е фрагментов точно был только один SplashFragment. Откуда взялась вторая сущность? Оказалось, что первый объект представляет собой NavGraph , который запустился в моей корневой Activity, а второй объект – мой SplashFragment, который представлен классом FragmentNavigator.Destination .
И тут у меня появилась идея – а что если вызвать на NavController -е функцию popBackStack и передать туда идентификатор графа? Коль скоро граф находится в back stack-е NavController -а, это должно удалить все экраны, которые были добавлены в рамках этого графа.
И эта идея сработала.
Минус такого подхода к определению обратной навигации очевиден: эта навигация не отобразится в визуальном редакторе. Конечно, можно определить action в XML-е вот таким образом:
В таком случае мы сможем использовать для обратной навигации NavController :
Но есть в этом что-то семантически неправильное: странно использовать слово navigate для закрытия экранов и обратной навигации.
Возврат результата из вложенного флоу
Что ж, мы получили некоторое подобие обратной навигации. Но возникает ещё один вопрос: есть ли способ вернуть из вложенного флоу какой-нибудь результат?
Да, есть. В Navigation Component 2.3 Google представил нам специальное key-value хранилище для проброса результатов с других экранов – SavedStateHandle . К этому хранилищу можно получить доступ через свойства NavController -а – previousBackStackEntry и currentBackStackEntry . Но в своих примерах Google почему-то считает, что ваш вложенный флоу всегда состоит только из одного экрана.
Что делать, если вложенный флоу состоит из нескольких экранов? Вы не можете использовать previousBackStackEntry для доступа к SavedStateHandle , потому что в этом случае вы положите данные в один из экранов вашего вложенного флоу. Для решения этой проблемы можно воспользоваться следующим фиксом:
Суть в следующем: до вызова findNavController().popBackStack вы находитесь ВНУТРИ вашего флоу экранов, а вот сразу после вызова popBackStack – уже на экране, который НАЧАЛ ваш флоу! И это означает, что вы можете использовать для доступа к SavedStateHandle свойство currentBackStackEntry . Этот entry будет означать ваш стартовый экран, которому нужен результат из флоу.
В свою очередь, на на экране, который начал вложенный флоу, вы тоже используете currentBackStackEntry для доступа к SavedStateHandle . И, следовательно, читаете правильные данные:
Выводы по работе с вложенным флоу
Навигация из вложенного графа во внешний граф
Сейчас будет немного терминологии, чтобы синхронизировать наше понимание по поводу графов навигации.
Пусть у нас два графа навигации – граф A и граф B. Я буду называть граф B вложенным в граф A, если мы вкладываем его через include . И, наоборот, я буду называть граф A внешним по отношению к графу B, если граф А включает в себя граф B.
Граф B – вложенный в граф A:
Граф А – внешний по отношению к графу B:
А теперь давайте разберём кейс навигации из вложенного графа во внешний граф.
Допустим, у нас есть вкладка BottomNavigationView, с которой мы хотим открыть последовательность из двух экранов. После второго экрана мы должны вернуться на вкладку, которая начала эту последовательность…
Что? В смысле, «это тот самый первый кейс, который ты уже разобрал»? Разве вы не заметили, что у этой последовательности НЕТ нижней навигации?
Смотрите, вот экран с нижней навигацией:
А вот последовательность экранов без неё:
Мы хотим открыть последовательность экранов поверх имеющегося. Если мы просто вложим Auth-граф в граф навигации, относящийся к вкладке нижней навигации, то мы получим не тот результат, который хотим: экраны Auth-графа будут иметь нижнюю навигацию.
Пусть мы вставили граф auth flow-навигации в наш граф вкладки нижней навигации и добавили action для перехода в него:
В этом случае первый экран auth-флоу появится в контейнере с нижней навигацией, а мы этого не хотели:
При этом не хочется переносить логику работы с нижней навигацией в Activity, писать какие-то методы по скрытию / демонстрации этого BottomNavigationView. Не зря же я адаптировал фикс из NavigationAdvancedSample для фрагмента, в конце концов.
В каком случае мы получим желаемый результат? Если каким-то образом осуществим навигацию из контейнера с BottomNavigationView (не из самой вкладки, а из контейнера, который является Host-ом для всех этих вкладок), то Auth-граф откроется без нижней навигации.
Если у нас получится сделать навигацию по зелёной стрелочке, то мы сможем перейти от контейнера с нижней навигацией в контейнер без неё.
Давайте введём action для навигации между MainFragment-ом и флоу авторизации:
Но проблема в том, что если мы попытаемся использовать этот action прямо из вкладки нижней навигации, вот так:
… то приложение упадёт с IllegalArgumentException , потому что NavController текущей вкладки ничего не знает о навигации вне своего Host-а навигации.
Ищем «правильный» NavController
Оказалось, что проблему можно решить, если найти «правильный» NavController, знающий про описанный вами action и привязанный к «внешнему» (с точки зрения фрагмента вкладки) для нас хосту. Если мы запустим action через него, навигация пройдёт ровно так, как нам нужно.
В Navigation Component есть специальная утилитная функция для поиска NavController -а, который привязан к нужному вам контейнеру, – Navigation.findNavController :
Проблемы с навигацией по кнопке Back
Итак, мы смогли открыть флоу авторизации поверх открытого фрагмента с нижней навигацией. Но появилась новая проблема: если пользователь нажмёт кнопку «Back», находясь на первом экране графа авторизации, приложение упадёт. Снова с IllegalArgumentException – на этот раз NavController не может найти контейнер, с которого мы только что пришли, как будто мы используем неправильный NavController для обратной навигации.
Исключение, которое мы получаем:
Эту проблему можно решить, переопределив поведение кнопки «Back». В одной из новых версий AndroidX появился удобный OnBackPressedCallback . Раз мы используем неправильный NavController по умолчанию, значит, мы можем подменить его на правильный:
В нашем callback-е мы делаем ровно то же, что и в прошлый раз, когда пытались построить навигацию от вкладки нижней навигации в auth-граф: находим «правильный» NavController , который привёл нас на первый экран внешнего графа навигации, чтобы именно с его помощью вернуться назад.
И это работает! Но есть одно «но»: чтобы это продолжало работать на протяжении всего auth-флоу, нам надо добавить точно такой же OnBackPressedCallback в каждый экран этого флоу =(
И, конечно же, придётся поправить закрытие всего auth-флоу – там мы тоже должны добавить получение «правильного» NavController -а:
Подведём итоги
Навигация по условию
Допустим, на старте приложения мы показываем пользователю экран Splash-а. На нём мы выполняем действия, связанные с инициализацией приложения. Потом, если пользователь не авторизован, мы хотим перевести его во флоу авторизации, в противном случае – сразу покажем экран с нижней навигацией. При этом, когда пользователь завершит флоу авторизации (неважно, как именно), мы должны показать ему главный экран с нижней навигацией.
У этого кейса есть пара особенностей, которые отличают его от первого рассмотренного случая.
- Когда пользователь нажмёт на кнопку «Back» на первом экране флоу авторизации, мы хотим не «вернуться назад» (потому что зачем нам второй раз показывать Splash), а закрыть приложение.
- После завершения флоу авторизации мы не просто закрываем открытый нами граф, но и двигаемся вперёд.
С первым пунктом проблем быть не должно, мы можем просто пробросить флажок в флоу авторизации, который будет говорить в OnBackPressedCallback , когда надо закрывать приложение, а когда просто двигаться назад:
Определяем флажок для StartAuthFragment:
А теперь используем этот флажок в OnBackPressedCallback :
Поскольку у нас Single Activity, requireActivity().finish() будет достаточно, чтобы закрыть наше приложение.
Со вторым пунктом чуть интереснее. Я вижу два способа реализовать такую «пост-навигацию».
- Первый способ: Navigation Component позволяет в runtime-е менять граф навигации, мы могли бы где-нибудь сохранить @id будущего destination-а и добавить немного логики при завершении авторизации.
- Второй способ – закрывать флоу авторизации как и раньше, а логику движения вперёд дописать в экран, который стартовал экраны авторизации, то есть в Splash.
Первый способ мне не нравится тем, что если появятся дополнительные destination-ы, которые надо открывать после экранов авторизации, появится и много лишней логики внутри флоу авторизации. Да и модифицировать граф навигации в runtime-е — то ещё удовольствие.
Второй способ тоже не ахти – потребуется сохранить предыдущий экран в back stack-е, чтобы, вернувшись на него и прочитав результат после авторизации, мы могли двигаться дальше. Но это всё равно приемлемый вариант: вложенный флоу будет отвечать только за свою собственную логику, а экран, который начинает подобную «условную» навигацию (выбор между main и auth на Splash-е, например), и так знает, как двигаться вперёд.
И реализовать это просто – мы знаем, как закрыть auth-флоу, знаем, как прокинуть из него результат на экран, который стартовал экраны авторизации. Останется только поймать результат на SplashFragment -е.
Пробрасываем результат из auth-флоу:
И ловим его на стороне SplashFragment -а:
Выводы по кейсам вложенной навигации
Через неделю, в заключительной статье о Navigation Component, я расскажу, как можно организовать навигацию в многомодульных приложениях и как работается с дип линками, а также покажу кейсы со встраиваемыми фрагментами и диалогами.
Источник