[Разработка под Android] Navigation Component-дзюцу, vol. 2 – вложенные графы навигации
Автор
Сообщение
news_bot ®
Стаж: 6 лет 9 месяцев
Сообщений: 27286
Каждое большое приложение содержит множество способов навигации между экранами. А хорошая библиотека навигации должна помогать разработчику их реализовывать. Именно с такой мыслью я подошёл к исследованию кейсов со вложенными графами навигации.
Это вторая из трёх статей про реализацию кейсов навигации при помощи Navigation Component-а.
Первая статья про BottomNavigationView.
Где на схеме приложения кейсы со вложенными графами?
SPL
Навигация во вложенный граф и обратно
В этом кейсе мы говорим про следующую часть общей схемы навигации:
Представим такую ситуацию: у нас есть 4 экрана — A, B, C и D. Пусть с экранов A и B вы можете перейти на экран C, с экрана C в экран D, а после D — вернуться на тот экран, который начал флоу C->D.
А можно нагляднее?
SPL
В тестовом приложении, которое я приготовил для разбора Navigation Component-а, есть две вкладки BottomNavigationView (на схеме это Search и Responses – но пусть они будут экранами A и B):
С обеих этих вкладок мы можем перейти на некоторый вложенный флоу, который состоит из двух экранов (C и D):
Если мы перейдём на экран C с вкладки Search (экрана A), то после экрана D мы должны вернуться на вкладку Search:
А если мы стартуем экран C со вкладки Responses, то после завершения внутреннего флоу C->D мы должны вернуться на вкладку Responses:
Этот кейс описывает старт последовательности экранов из разных мест приложения, а после её завершения возврат на тот экран, который начал эту последовательность.
Как это реализовать? Для начала вы объявляете вашу «вложенную» последовательность экранов в отдельном XML-файле навигации, чтобы можно было вкладывать её в другие графы:
Объявление графа вложенной навигации
SPL
<!-- company_flow__nav_graph.xml -->
<navigation
android:id="@+id/company_flow__nav_graph"
app:startDestination="@id/CompanyFragment">
<fragment
android:id="@+id/CompanyFragment"
android:name="ui.company.CompanyFragment">
<action
android:id="@+id/action__CompanyFragment__to__CompanyDetailsFragment"
app:destination="@id/CompanyDetailsFragment" />
</fragment>
<fragment
android:id="@+id/CompanyDetailsFragment"
android:name="ui.company.CompanyDetailsFragment"/>
</navigation>
Затем следует вложить созданный граф навигации в уже существующий граф и использовать идентификатор вложенного графа для описания action-ов:
Добавление графа навигации в другой граф
SPL
<navigation
android:id="@+id/menu__search"
app:startDestination="@id/SearchContainerFragment">
<fragment
android:id="@+id/SearchContainerFragment"
android:name="ui.tabs.search.SearchContainerFragment">
<action
android:id="@+id/action__SearchContainerFragment__to__CompanyFlow"
app:destination="@id/company_flow__nav_graph" />
</fragment>
<include app:graph="@navigation/company_flow__nav_graph" />
</navigation>
Итак, вы описали навигацию из двух разных мест приложения во вложенный флоу. Но что делать с возвратом?
Проблема в том, что Navigation Component не позволяет нормально описывать навигацию НАЗАД, только навигацию ВПЕРЁД. Но при этом даёт возможность описывать удаление экранов из back stack-а при помощи атрибутов popBackUp и popBackUpInclusive в XML, а также при помощи функции popBackStack в NavController-е.
Пока размышлял над этим, заметил интересную вещь: я подключился дебаггером перед переходом с экрана Splash на экран с нижней навигацией и обнаружил, что поле mBackStack внутри NavController-а Splash-экрана содержит два объекта NavBackStackEntry.
А можно на картинке?
SPL
Честно говоря, я не ожидал увидеть там два объекта, поскольку в back stack-е фрагментов точно был только один SplashFragment. Откуда взялась вторая сущность? Оказалось, что первый объект представляет собой NavGraph, который запустился в моей корневой Activity, а второй объект – мой SplashFragment, который представлен классом FragmentNavigator.Destination.
И тут у меня появилась идея – а что если вызвать на NavController-е функцию popBackStack и передать туда идентификатор графа? Коль скоро граф находится в back stack-е NavController-а, это должно удалить все экраны, которые были добавлены в рамках этого графа.
И эта идея сработала.
Возврат из flow при помощи popBackStack
SPL
class CompanyDetailsFragment : Fragment(R.layout.fragment_company_details) {
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
finish_flow_button.setOnClickListener {
findNavController().popBackStack(R.id.company_flow__nav_graph, true)
}
}
}
Минус такого подхода к определению обратной навигации очевиден: эта навигация не отобразится в визуальном редакторе. Конечно, можно определить action в XML-е вот таким образом:
Определение action-а для закрытия графа навигации
SPL
<fragment
android:id="@+id/CompanyDetailsFragment"
android:name="ui.company.CompanyDetailsFragment"
android:label="@string/fragment_company_details__title"
tools:layout="@layout/fragment_company_details">
<action
android:id="@+id/action__finishCompanyFlow"
app:popUpTo="@id/company_flow__nav_graph"
app:popUpToInclusive="true" />
</fragment>
В таком случае мы сможем использовать для обратной навигации NavController:
findNavController().navigate(R.id.action__finishCompanyFlow)
Но есть в этом что-то семантически неправильное: странно использовать слово navigate для закрытия экранов и обратной навигации.
Возврат результата из вложенного флоу
Что ж, мы получили некоторое подобие обратной навигации. Но возникает ещё один вопрос: есть ли способ вернуть из вложенного флоу какой-нибудь результат?
Да, есть. В Navigation Component 2.3 Google представил нам специальное key-value хранилище для проброса результатов с других экранов – SavedStateHandle. К этому хранилищу можно получить доступ через свойства NavController-а – previousBackStackEntry и currentBackStackEntry. Но в своих примерах Google почему-то считает, что ваш вложенный флоу всегда состоит только из одного экрана.
Типичный пример работы с SavedStateHandle
SPL
// Flow screen
findNavController().previousBackStackEntry
?.savedStateHandle
?.set("some_key", "value")
// Screen that waits result
val result = findNavController().currentBackStackEntry
?.savedStateHandle
?.remove<String>("some_key")
Что делать, если вложенный флоу состоит из нескольких экранов? Вы не можете использовать previousBackStackEntry для доступа к SavedStateHandle, потому что в этом случае вы положите данные в один из экранов вашего вложенного флоу. Для решения этой проблемы можно воспользоваться следующим фиксом:
Посмотреть на фикс
SPL
fragment_company_details__button.setOnClickListener {
// Here we are inside nested navigation flow
findNavController().popBackStack(R.id.company_flow__nav_graph, true)
// At this line, "findNavController().currentBackStackEntry" means
// screen that STARTED current nested flow.
// So we can send the result!
findNavController().currentBackStackEntry
?.savedStateHandle
?.set(COMPANY_FLOW_RESULT_FLAG, true)
}
Суть в следующем: до вызова findNavController().popBackStack вы находитесь ВНУТРИ вашего флоу экранов, а вот сразу после вызова popBackStack – уже на экране, который НАЧАЛ ваш флоу! И это означает, что вы можете использовать для доступа к SavedStateHandle свойство currentBackStackEntry. Этот entry будет означать ваш стартовый экран, которому нужен результат из флоу.
В свою очередь, на на экране, который начал вложенный флоу, вы тоже используете currentBackStackEntry для доступа к SavedStateHandle. И, следовательно, читаете правильные данные:
Читаем данные из SavedStateHandle
SPL
// Read result from nested navigation flow
val companyFlowResult = findNavController().currentBackStackEntry
?.savedStateHandle
?.remove<Boolean>(CompanyDetailsFragment.COMPANY_FLOW_RESULT_FLAG)
text__company_flow_result.text = "${companyFlowResult}"
Выводы по работе с вложенным флоу
- Для обратной навигации из вложенного флоу, состоящего из нескольких экранов, можно использовать функцию NavController.popBackStack, передав туда идентификатор графа навигации вашего флоу.
- Для проброса какого-либо результата из вложенного флоу можно использовать SavedStateHandle.
Навигация из вложенного графа во внешний граф
Сейчас будет немного терминологии, чтобы синхронизировать наше понимание по поводу графов навигации.
Пусть у нас два графа навигации – граф A и граф B. Я буду называть граф B вложенным в граф A, если мы вкладываем его через include. И, наоборот, я буду называть граф A внешним по отношению к графу B, если граф А включает в себя граф B.
Ещё немного картинок
SPL
А теперь давайте разберём кейс навигации из вложенного графа во внешний граф.
Допустим, у нас есть вкладка BottomNavigationView, с которой мы хотим открыть последовательность из двух экранов. После второго экрана мы должны вернуться на вкладку, которая начала эту последовательность…
Что? В смысле, «это тот самый первый кейс, который ты уже разобрал»? Разве вы не заметили, что у этой последовательности НЕТ нижней навигации?
Приблизить картинку
SPL
Мы хотим открыть последовательность экранов поверх имеющегося. Если мы просто вложим Auth-граф в граф навигации, относящийся к вкладке нижней навигации, то мы получим не тот результат, который хотим: экраны Auth-графа будут иметь нижнюю навигацию.
Неправильный подход к такой навигации
SPL
Пусть мы вставили граф auth flow-навигации в наш граф вкладки нижней навигации и добавили action для перехода в него:
<navigation xmlns:android="http://schemas.android.com/apk/res/android"
android:id="@+id/menu__profile"
app:startDestination="@id/ProfileContainerFragment">
<fragment
android:id="@+id/ProfileContainerFragment"
android:name="ui.tabs.profile.ProfileContainerFragment">
<action
android:id="@+id/action__ProfileContainerFragment__to__AuthFlow"
app:destination="@id/auth__nav_graph" />
</fragment>
<include app:graph="@navigation/auth__nav_graph" />
</navigation>
В этом случае первый экран auth-флоу появится в контейнере с нижней навигацией, а мы этого не хотели:
При этом не хочется переносить логику работы с нижней навигацией в Activity, писать какие-то методы по скрытию / демонстрации этого BottomNavigationView. Не зря же я адаптировал фикс из NavigationAdvancedSample для фрагмента, в конце концов.
В каком случае мы получим желаемый результат? Если каким-то образом осуществим навигацию из контейнера с BottomNavigationView (не из самой вкладки, а из контейнера, который является Host-ом для всех этих вкладок), то Auth-граф откроется без нижней навигации.
А на картинке можно?
SPL
Давайте введём action для навигации между MainFragment-ом и флоу авторизации:
Описание навигации
SPL
<!— app_nav_graph.xml —>
<fragment
android:id="@+id/SplashFragment"
android:name="com.aaglobal.jnc_playground.ui.splash.SplashFragment"/>
<fragment
android:id="@+id/MainFragment"
android:name="com.aaglobal.jnc_playground.ui.main.MainFragment">
<action
android:id="@+id/action__MainFragment__to__AuthFlow"
app:destination="@id/auth__nav_graph" />
</fragment>
<include app:graph="@navigation/auth__nav_graph" />
Но проблема в том, что если мы попытаемся использовать этот action прямо из вкладки нижней навигации, вот так:
fragment_profile_container__button__open_auth_flow.setOnClickListener {
findNavController().navigate(R.id.action__MainFragment__to__AuthFlow)
}
… то приложение упадёт с IllegalArgumentException, потому что NavController текущей вкладки ничего не знает о навигации вне своего Host-а навигации.
Ищем «правильный» NavController
Оказалось, что проблему можно решить, если найти «правильный» NavController, знающий про описанный вами action и привязанный к «внешнему» (с точки зрения фрагмента вкладки) для нас хосту. Если мы запустим action через него, навигация пройдёт ровно так, как нам нужно.
В Navigation Component есть специальная утилитная функция для поиска NavController-а, который привязан к нужному вам контейнеру, – Navigation.findNavController:
Открываем флоу авторизации правильно
SPL
Проблемы с навигацией по кнопке Back
Итак, мы смогли открыть флоу авторизации поверх открытого фрагмента с нижней навигацией. Но появилась новая проблема: если пользователь нажмёт кнопку «Back», находясь на первом экране графа авторизации, приложение упадёт. Снова с IllegalArgumentException – на этот раз NavController не может найти контейнер, с которого мы только что пришли, как будто мы используем неправильный NavController для обратной навигации.
Покажи гифку
SPL
Эту проблему можно решить, переопределив поведение кнопки «Back». В одной из новых версий AndroidX появился удобный OnBackPressedCallback. Раз мы используем неправильный NavController по умолчанию, значит, мы можем подменить его на правильный:
Переопределяем back-навигацию для первого экрана auth-графа
SPL
class StartAuthFragment : Fragment(R.layout.fragment_start_auth) {
private var callback: OnBackPressedCallback? = null
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
callback = object : OnBackPressedCallback(true) {
override fun handleOnBackPressed() {
Navigation.findNavController(
requireActivity(),
R.id.activity_root__fragment__nav_host
).popBackStack()
}
}.also {
requireActivity().onBackPressedDispatcher.addCallback(viewLifecycleOwner, it)
}
}
}
В нашем callback-е мы делаем ровно то же, что и в прошлый раз, когда пытались построить навигацию от вкладки нижней навигации в auth-граф: находим «правильный» NavController, который привёл нас на первый экран внешнего графа навигации, чтобы именно с его помощью вернуться назад.
И это работает! Но есть одно «но»: чтобы это продолжало работать на протяжении всего auth-флоу, нам надо добавить точно такой же OnBackPressedCallback в каждый экран этого флоу =(
И, конечно же, придётся поправить закрытие всего auth-флоу – там мы тоже должны добавить получение «правильного» NavController-а:
Как это выглядит?
SPL
class FinishAuthFragment : Fragment(R.layout.fragment_finish_auth) {
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
fragment_finish_auth__button.setOnClickListener {
Navigation.findNavController(
requireActivity(),
R.id.activity_root__fragment__nav_host
).popBackStack(R.id.auth__nav_graph, true)
findNavController().currentBackStackEntry
?.savedStateHandle
?.set(AUTH_FLOW_RESULT_KEY, true)
}
}
}
Подведём итоги
- Если хотите осуществить навигацию вне текущего контейнера навигации, вы можете это сделать, получив «правильный» NavController.
- Помните, что это вызовет проблемы с обратной навигацией.
Навигация по условию
Допустим, на старте приложения мы показываем пользователю экран Splash-а. На нём мы выполняем действия, связанные с инициализацией приложения. Потом, если пользователь не авторизован, мы хотим перевести его во флоу авторизации, в противном случае – сразу покажем экран с нижней навигацией. При этом, когда пользователь завершит флоу авторизации (неважно, как именно), мы должны показать ему главный экран с нижней навигацией.
Покажи на картинке
SPL
У этого кейса есть пара особенностей, которые отличают его от первого рассмотренного случая.
- Когда пользователь нажмёт на кнопку «Back» на первом экране флоу авторизации, мы хотим не «вернуться назад» (потому что зачем нам второй раз показывать Splash), а закрыть приложение.
- После завершения флоу авторизации мы не просто закрываем открытый нами граф, но и двигаемся вперёд.
С первым пунктом проблем быть не должно, мы можем просто пробросить флажок в флоу авторизации, который будет говорить в OnBackPressedCallback, когда надо закрывать приложение, а когда просто двигаться назад:
Покажи код
SPL
Определяем флажок для StartAuthFragment:
<fragment
android:id="@+id/StartAuthFragment"
android:name="com.aaglobal.jnc_playground.ui.auth.StartAuthFragment"
android:label="Start auth"
tools:layout="@layout/fragment_start_auth">
<argument
android:name="isFromSplashScreen"
android:defaultValue="false"
app:argType="boolean"
app:nullable="false" />
<action
android:id="@+id/action__StartAuthFragment__to__FinishAuthFragment"
app:destination="@id/FinishAuthFragment" />
</fragment>
А теперь используем этот флажок в OnBackPressedCallback:
class StartAuthFragment : Fragment(R.layout.fragment_start_auth) {
private val args: StartAuthFragmentArgs by navArgs()
private var callback: OnBackPressedCallback? = null
private fun getOnBackPressedCallback(): OnBackPressedCallback {
return object : OnBackPressedCallback(true) {
override fun handleOnBackPressed() {
if (args.isFromSplashScreen) {
requireActivity().finish()
} else {
Navigation.findNavController(
requireActivity(),
R.id.activity_root__fragment__nav_host
).popBackStack()
}
}
}
}
}
Поскольку у нас Single Activity, requireActivity().finish() будет достаточно, чтобы закрыть наше приложение.
Со вторым пунктом чуть интереснее. Я вижу два способа реализовать такую «пост-навигацию».
- Первый способ: Navigation Component позволяет в runtime-е менять граф навигации, мы могли бы где-нибудь сохранить @id будущего destination-а и добавить немного логики при завершении авторизации.
- Второй способ – закрывать флоу авторизации как и раньше, а логику движения вперёд дописать в экран, который стартовал экраны авторизации, то есть в Splash.
Первый способ мне не нравится тем, что если появятся дополнительные destination-ы, которые надо открывать после экранов авторизации, появится и много лишней логики внутри флоу авторизации. Да и модифицировать граф навигации в runtime-е — то ещё удовольствие.
Второй способ тоже не ахти – потребуется сохранить предыдущий экран в back stack-е, чтобы, вернувшись на него и прочитав результат после авторизации, мы могли двигаться дальше. Но это всё равно приемлемый вариант: вложенный флоу будет отвечать только за свою собственную логику, а экран, который начинает подобную «условную» навигацию (выбор между main и auth на Splash-е, например), и так знает, как двигаться вперёд.
И реализовать это просто – мы знаем, как закрыть auth-флоу, знаем, как прокинуть из него результат на экран, который стартовал экраны авторизации. Останется только поймать результат на SplashFragment-е.
Покажи код
SPL
Пробрасываем результат из auth-флоу:
// FinishAuthFragment.kt
fragment_finish_auth__button.setOnClickListener {
// Save hasAuthData flag in prefs
GlobalDI.getAuthRepository().putHasAuthDataFlag(true)
// Navigate back from auth flow
Navigation.findNavController(
requireActivity(),
R.id.activity_root__fragment__nav_host
).popBackStack(R.id.auth__nav_graph, true)
// Send signal about finishing flow
findNavController().currentBackStackEntry
?.savedStateHandle
?.set(AUTH_FLOW_RESULT_KEY, true)
}
И ловим его на стороне SplashFragment-а:
// SplashFragment.kt
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
val authResult = findNavController().currentBackStackEntry
?.savedStateHandle
?.remove<Boolean>(FinishAuthFragment.AUTH_FLOW_RESULT_KEY) == true
if (authResult) {
navigateToMainScreen()
return
}
}
Выводы по кейсам вложенной навигации
- У подобных кейсов куча подводных камней – тестировать нужно внимательно.
- Вы должны быть морально готовы искать решения для ваших случаев, ведь я столкнулся далеко не со всеми возможными проблемами.
Через неделю, в заключительной статье о Navigation Component, я расскажу, как можно организовать навигацию в многомодульных приложениях и как работается с дип линками, а также покажу кейсы со встраиваемыми фрагментами и диалогами.
===========
Источник:
habr.com
===========
Похожие новости:
- [Разработка под Android] Антипаттерн “Репозиторий” в Android (перевод)
- [Разработка мобильных приложений, Разработка под Android, Смартфоны] Обзор HMS Core 5.0: ещё больше возможностей для ML на мобильных устройствах и новые инструменты для аудио и видео
- [Программирование, Разработка под Android, Разработка мобильных приложений] 20 инструментов Android-разработчика, о которых вы могли не знать (перевод)
- [Автомобильные гаджеты, Разработка под Android, Гаджеты, Транспорт] Пользователи массово жалуются на проблемы Android Auto в Android 11
- [Разработка под Android] Превращаем EditText в SearchEditText
- [Производство и разработка электроники, Научно-популярное, Биотехнологии, Здоровье] Нет реальности без боли: электронный эквивалент рецепторов кожи человека
- [Java, Open source, Openshift, Учебный процесс в IT] 10 Kubernetes-инструментов из разряда «важно», шпаргалка по созданию Kubernetes-операторов на Java… и многое другое
- [Браузеры, Разработка под Android] Vivaldi 3.3 для Android — Панельная свобода
- [Информационная безопасность, Разработка под iOS, Разработка под Android, Реверс-инжиниринг, Аналитика мобильных приложений] Домофоны, СКУД… И снова здравствуйте
- [Разработка мобильных приложений, Разработка под Android, Системы обмена сообщениями] Google без предупреждения удалила из магазина приложений почтовый сервис K-9 Mail за разное написание названия
Теги для поиска: #_razrabotka_pod_android (Разработка под Android), #_android, #_navigation_component, #_bagi (баги), #_bol (боль), #_blog_kompanii_headhunter (
Блог компании HeadHunter
), #_razrabotka_pod_android (
Разработка под Android
)
Вы не можете начинать темы
Вы не можете отвечать на сообщения
Вы не можете редактировать свои сообщения
Вы не можете удалять свои сообщения
Вы не можете голосовать в опросах
Вы не можете прикреплять файлы к сообщениям
Вы не можете скачивать файлы
Текущее время: 23-Ноя 00:09
Часовой пояс: UTC + 5
Автор | Сообщение |
---|---|
news_bot ®
Стаж: 6 лет 9 месяцев |
|
Каждое большое приложение содержит множество способов навигации между экранами. А хорошая библиотека навигации должна помогать разработчику их реализовывать. Именно с такой мыслью я подошёл к исследованию кейсов со вложенными графами навигации. Это вторая из трёх статей про реализацию кейсов навигации при помощи Navigation Component-а. Первая статья про BottomNavigationView. Где на схеме приложения кейсы со вложенными графами?SPLНавигация во вложенный граф и обратно В этом кейсе мы говорим про следующую часть общей схемы навигации: Представим такую ситуацию: у нас есть 4 экрана — A, B, C и D. Пусть с экранов A и B вы можете перейти на экран C, с экрана C в экран D, а после D — вернуться на тот экран, который начал флоу C->D. А можно нагляднее?SPLВ тестовом приложении, которое я приготовил для разбора Navigation Component-а, есть две вкладки BottomNavigationView (на схеме это Search и Responses – но пусть они будут экранами A и B):
С обеих этих вкладок мы можем перейти на некоторый вложенный флоу, который состоит из двух экранов (C и D): Если мы перейдём на экран C с вкладки Search (экрана A), то после экрана D мы должны вернуться на вкладку Search: А если мы стартуем экран C со вкладки Responses, то после завершения внутреннего флоу C->D мы должны вернуться на вкладку Responses: Этот кейс описывает старт последовательности экранов из разных мест приложения, а после её завершения возврат на тот экран, который начал эту последовательность. Как это реализовать? Для начала вы объявляете вашу «вложенную» последовательность экранов в отдельном XML-файле навигации, чтобы можно было вкладывать её в другие графы: Объявление графа вложенной навигацииSPL<!-- company_flow__nav_graph.xml -->
<navigation android:id="@+id/company_flow__nav_graph" app:startDestination="@id/CompanyFragment"> <fragment android:id="@+id/CompanyFragment" android:name="ui.company.CompanyFragment"> <action android:id="@+id/action__CompanyFragment__to__CompanyDetailsFragment" app:destination="@id/CompanyDetailsFragment" /> </fragment> <fragment android:id="@+id/CompanyDetailsFragment" android:name="ui.company.CompanyDetailsFragment"/> </navigation> Затем следует вложить созданный граф навигации в уже существующий граф и использовать идентификатор вложенного графа для описания action-ов: Добавление графа навигации в другой графSPL<navigation
android:id="@+id/menu__search" app:startDestination="@id/SearchContainerFragment"> <fragment android:id="@+id/SearchContainerFragment" android:name="ui.tabs.search.SearchContainerFragment"> <action android:id="@+id/action__SearchContainerFragment__to__CompanyFlow" app:destination="@id/company_flow__nav_graph" /> </fragment> <include app:graph="@navigation/company_flow__nav_graph" /> </navigation> Итак, вы описали навигацию из двух разных мест приложения во вложенный флоу. Но что делать с возвратом? Проблема в том, что Navigation Component не позволяет нормально описывать навигацию НАЗАД, только навигацию ВПЕРЁД. Но при этом даёт возможность описывать удаление экранов из back stack-а при помощи атрибутов popBackUp и popBackUpInclusive в XML, а также при помощи функции popBackStack в NavController-е. Пока размышлял над этим, заметил интересную вещь: я подключился дебаггером перед переходом с экрана Splash на экран с нижней навигацией и обнаружил, что поле mBackStack внутри NavController-а Splash-экрана содержит два объекта NavBackStackEntry. А можно на картинке?SPLЧестно говоря, я не ожидал увидеть там два объекта, поскольку в back stack-е фрагментов точно был только один SplashFragment. Откуда взялась вторая сущность? Оказалось, что первый объект представляет собой NavGraph, который запустился в моей корневой Activity, а второй объект – мой SplashFragment, который представлен классом FragmentNavigator.Destination. И тут у меня появилась идея – а что если вызвать на NavController-е функцию popBackStack и передать туда идентификатор графа? Коль скоро граф находится в back stack-е NavController-а, это должно удалить все экраны, которые были добавлены в рамках этого графа. И эта идея сработала. Возврат из flow при помощи popBackStackSPLclass CompanyDetailsFragment : Fragment(R.layout.fragment_company_details) {
override fun onViewCreated(view: View, savedInstanceState: Bundle?) { super.onViewCreated(view, savedInstanceState) finish_flow_button.setOnClickListener { findNavController().popBackStack(R.id.company_flow__nav_graph, true) } } } Минус такого подхода к определению обратной навигации очевиден: эта навигация не отобразится в визуальном редакторе. Конечно, можно определить action в XML-е вот таким образом: Определение action-а для закрытия графа навигацииSPL<fragment
android:id="@+id/CompanyDetailsFragment" android:name="ui.company.CompanyDetailsFragment" android:label="@string/fragment_company_details__title" tools:layout="@layout/fragment_company_details"> <action android:id="@+id/action__finishCompanyFlow" app:popUpTo="@id/company_flow__nav_graph" app:popUpToInclusive="true" /> </fragment> В таком случае мы сможем использовать для обратной навигации NavController: findNavController().navigate(R.id.action__finishCompanyFlow)
Но есть в этом что-то семантически неправильное: странно использовать слово navigate для закрытия экранов и обратной навигации. Возврат результата из вложенного флоу Что ж, мы получили некоторое подобие обратной навигации. Но возникает ещё один вопрос: есть ли способ вернуть из вложенного флоу какой-нибудь результат? Да, есть. В Navigation Component 2.3 Google представил нам специальное key-value хранилище для проброса результатов с других экранов – SavedStateHandle. К этому хранилищу можно получить доступ через свойства NavController-а – previousBackStackEntry и currentBackStackEntry. Но в своих примерах Google почему-то считает, что ваш вложенный флоу всегда состоит только из одного экрана. Типичный пример работы с SavedStateHandleSPL// Flow screen
findNavController().previousBackStackEntry ?.savedStateHandle ?.set("some_key", "value") // Screen that waits result val result = findNavController().currentBackStackEntry ?.savedStateHandle ?.remove<String>("some_key") Что делать, если вложенный флоу состоит из нескольких экранов? Вы не можете использовать previousBackStackEntry для доступа к SavedStateHandle, потому что в этом случае вы положите данные в один из экранов вашего вложенного флоу. Для решения этой проблемы можно воспользоваться следующим фиксом: Посмотреть на фиксSPLfragment_company_details__button.setOnClickListener {
// Here we are inside nested navigation flow findNavController().popBackStack(R.id.company_flow__nav_graph, true) // At this line, "findNavController().currentBackStackEntry" means // screen that STARTED current nested flow. // So we can send the result! findNavController().currentBackStackEntry ?.savedStateHandle ?.set(COMPANY_FLOW_RESULT_FLAG, true) } Суть в следующем: до вызова findNavController().popBackStack вы находитесь ВНУТРИ вашего флоу экранов, а вот сразу после вызова popBackStack – уже на экране, который НАЧАЛ ваш флоу! И это означает, что вы можете использовать для доступа к SavedStateHandle свойство currentBackStackEntry. Этот entry будет означать ваш стартовый экран, которому нужен результат из флоу. В свою очередь, на на экране, который начал вложенный флоу, вы тоже используете currentBackStackEntry для доступа к SavedStateHandle. И, следовательно, читаете правильные данные: Читаем данные из SavedStateHandleSPL// Read result from nested navigation flow
val companyFlowResult = findNavController().currentBackStackEntry ?.savedStateHandle ?.remove<Boolean>(CompanyDetailsFragment.COMPANY_FLOW_RESULT_FLAG) text__company_flow_result.text = "${companyFlowResult}" Выводы по работе с вложенным флоу
Навигация из вложенного графа во внешний граф Сейчас будет немного терминологии, чтобы синхронизировать наше понимание по поводу графов навигации. Пусть у нас два графа навигации – граф A и граф B. Я буду называть граф B вложенным в граф A, если мы вкладываем его через include. И, наоборот, я буду называть граф A внешним по отношению к графу B, если граф А включает в себя граф B. Ещё немного картинокSPLА теперь давайте разберём кейс навигации из вложенного графа во внешний граф. Допустим, у нас есть вкладка BottomNavigationView, с которой мы хотим открыть последовательность из двух экранов. После второго экрана мы должны вернуться на вкладку, которая начала эту последовательность… Что? В смысле, «это тот самый первый кейс, который ты уже разобрал»? Разве вы не заметили, что у этой последовательности НЕТ нижней навигации? Приблизить картинкуSPLМы хотим открыть последовательность экранов поверх имеющегося. Если мы просто вложим Auth-граф в граф навигации, относящийся к вкладке нижней навигации, то мы получим не тот результат, который хотим: экраны Auth-графа будут иметь нижнюю навигацию. Неправильный подход к такой навигацииSPLПусть мы вставили граф auth flow-навигации в наш граф вкладки нижней навигации и добавили action для перехода в него:
<navigation xmlns:android="http://schemas.android.com/apk/res/android"
android:id="@+id/menu__profile" app:startDestination="@id/ProfileContainerFragment"> <fragment android:id="@+id/ProfileContainerFragment" android:name="ui.tabs.profile.ProfileContainerFragment"> <action android:id="@+id/action__ProfileContainerFragment__to__AuthFlow" app:destination="@id/auth__nav_graph" /> </fragment> <include app:graph="@navigation/auth__nav_graph" /> </navigation> В этом случае первый экран auth-флоу появится в контейнере с нижней навигацией, а мы этого не хотели: При этом не хочется переносить логику работы с нижней навигацией в Activity, писать какие-то методы по скрытию / демонстрации этого BottomNavigationView. Не зря же я адаптировал фикс из NavigationAdvancedSample для фрагмента, в конце концов. В каком случае мы получим желаемый результат? Если каким-то образом осуществим навигацию из контейнера с BottomNavigationView (не из самой вкладки, а из контейнера, который является Host-ом для всех этих вкладок), то Auth-граф откроется без нижней навигации. А на картинке можно?SPLДавайте введём action для навигации между MainFragment-ом и флоу авторизации: Описание навигацииSPL<!— app_nav_graph.xml —>
<fragment android:id="@+id/SplashFragment" android:name="com.aaglobal.jnc_playground.ui.splash.SplashFragment"/> <fragment android:id="@+id/MainFragment" android:name="com.aaglobal.jnc_playground.ui.main.MainFragment"> <action android:id="@+id/action__MainFragment__to__AuthFlow" app:destination="@id/auth__nav_graph" /> </fragment> <include app:graph="@navigation/auth__nav_graph" /> Но проблема в том, что если мы попытаемся использовать этот action прямо из вкладки нижней навигации, вот так: fragment_profile_container__button__open_auth_flow.setOnClickListener {
findNavController().navigate(R.id.action__MainFragment__to__AuthFlow) } … то приложение упадёт с IllegalArgumentException, потому что NavController текущей вкладки ничего не знает о навигации вне своего Host-а навигации. Ищем «правильный» NavController Оказалось, что проблему можно решить, если найти «правильный» NavController, знающий про описанный вами action и привязанный к «внешнему» (с точки зрения фрагмента вкладки) для нас хосту. Если мы запустим action через него, навигация пройдёт ровно так, как нам нужно. В Navigation Component есть специальная утилитная функция для поиска NavController-а, который привязан к нужному вам контейнеру, – Navigation.findNavController: Открываем флоу авторизации правильноSPLПроблемы с навигацией по кнопке Back Итак, мы смогли открыть флоу авторизации поверх открытого фрагмента с нижней навигацией. Но появилась новая проблема: если пользователь нажмёт кнопку «Back», находясь на первом экране графа авторизации, приложение упадёт. Снова с IllegalArgumentException – на этот раз NavController не может найти контейнер, с которого мы только что пришли, как будто мы используем неправильный NavController для обратной навигации. Покажи гифкуSPLЭту проблему можно решить, переопределив поведение кнопки «Back». В одной из новых версий AndroidX появился удобный OnBackPressedCallback. Раз мы используем неправильный NavController по умолчанию, значит, мы можем подменить его на правильный: Переопределяем back-навигацию для первого экрана auth-графаSPLclass StartAuthFragment : Fragment(R.layout.fragment_start_auth) {
private var callback: OnBackPressedCallback? = null override fun onViewCreated(view: View, savedInstanceState: Bundle?) { super.onViewCreated(view, savedInstanceState) callback = object : OnBackPressedCallback(true) { override fun handleOnBackPressed() { Navigation.findNavController( requireActivity(), R.id.activity_root__fragment__nav_host ).popBackStack() } }.also { requireActivity().onBackPressedDispatcher.addCallback(viewLifecycleOwner, it) } } } В нашем callback-е мы делаем ровно то же, что и в прошлый раз, когда пытались построить навигацию от вкладки нижней навигации в auth-граф: находим «правильный» NavController, который привёл нас на первый экран внешнего графа навигации, чтобы именно с его помощью вернуться назад. И это работает! Но есть одно «но»: чтобы это продолжало работать на протяжении всего auth-флоу, нам надо добавить точно такой же OnBackPressedCallback в каждый экран этого флоу =( И, конечно же, придётся поправить закрытие всего auth-флоу – там мы тоже должны добавить получение «правильного» NavController-а: Как это выглядит?SPLclass FinishAuthFragment : Fragment(R.layout.fragment_finish_auth) {
override fun onViewCreated(view: View, savedInstanceState: Bundle?) { super.onViewCreated(view, savedInstanceState) fragment_finish_auth__button.setOnClickListener { Navigation.findNavController( requireActivity(), R.id.activity_root__fragment__nav_host ).popBackStack(R.id.auth__nav_graph, true) findNavController().currentBackStackEntry ?.savedStateHandle ?.set(AUTH_FLOW_RESULT_KEY, true) } } } Подведём итоги
Навигация по условию Допустим, на старте приложения мы показываем пользователю экран Splash-а. На нём мы выполняем действия, связанные с инициализацией приложения. Потом, если пользователь не авторизован, мы хотим перевести его во флоу авторизации, в противном случае – сразу покажем экран с нижней навигацией. При этом, когда пользователь завершит флоу авторизации (неважно, как именно), мы должны показать ему главный экран с нижней навигацией. Покажи на картинкеSPLУ этого кейса есть пара особенностей, которые отличают его от первого рассмотренного случая.
С первым пунктом проблем быть не должно, мы можем просто пробросить флажок в флоу авторизации, который будет говорить в OnBackPressedCallback, когда надо закрывать приложение, а когда просто двигаться назад: Покажи кодSPLОпределяем флажок для StartAuthFragment:
<fragment
android:id="@+id/StartAuthFragment" android:name="com.aaglobal.jnc_playground.ui.auth.StartAuthFragment" android:label="Start auth" tools:layout="@layout/fragment_start_auth"> <argument android:name="isFromSplashScreen" android:defaultValue="false" app:argType="boolean" app:nullable="false" /> <action android:id="@+id/action__StartAuthFragment__to__FinishAuthFragment" app:destination="@id/FinishAuthFragment" /> </fragment> А теперь используем этот флажок в OnBackPressedCallback: class StartAuthFragment : Fragment(R.layout.fragment_start_auth) {
private val args: StartAuthFragmentArgs by navArgs() private var callback: OnBackPressedCallback? = null private fun getOnBackPressedCallback(): OnBackPressedCallback { return object : OnBackPressedCallback(true) { override fun handleOnBackPressed() { if (args.isFromSplashScreen) { requireActivity().finish() } else { Navigation.findNavController( requireActivity(), R.id.activity_root__fragment__nav_host ).popBackStack() } } } } } Поскольку у нас Single Activity, requireActivity().finish() будет достаточно, чтобы закрыть наше приложение. Со вторым пунктом чуть интереснее. Я вижу два способа реализовать такую «пост-навигацию».
Первый способ мне не нравится тем, что если появятся дополнительные destination-ы, которые надо открывать после экранов авторизации, появится и много лишней логики внутри флоу авторизации. Да и модифицировать граф навигации в runtime-е — то ещё удовольствие. Второй способ тоже не ахти – потребуется сохранить предыдущий экран в back stack-е, чтобы, вернувшись на него и прочитав результат после авторизации, мы могли двигаться дальше. Но это всё равно приемлемый вариант: вложенный флоу будет отвечать только за свою собственную логику, а экран, который начинает подобную «условную» навигацию (выбор между main и auth на Splash-е, например), и так знает, как двигаться вперёд. И реализовать это просто – мы знаем, как закрыть auth-флоу, знаем, как прокинуть из него результат на экран, который стартовал экраны авторизации. Останется только поймать результат на SplashFragment-е. Покажи кодSPLПробрасываем результат из auth-флоу:
// FinishAuthFragment.kt
fragment_finish_auth__button.setOnClickListener { // Save hasAuthData flag in prefs GlobalDI.getAuthRepository().putHasAuthDataFlag(true) // Navigate back from auth flow Navigation.findNavController( requireActivity(), R.id.activity_root__fragment__nav_host ).popBackStack(R.id.auth__nav_graph, true) // Send signal about finishing flow findNavController().currentBackStackEntry ?.savedStateHandle ?.set(AUTH_FLOW_RESULT_KEY, true) } И ловим его на стороне SplashFragment-а: // SplashFragment.kt
override fun onViewCreated(view: View, savedInstanceState: Bundle?) { super.onViewCreated(view, savedInstanceState) val authResult = findNavController().currentBackStackEntry ?.savedStateHandle ?.remove<Boolean>(FinishAuthFragment.AUTH_FLOW_RESULT_KEY) == true if (authResult) { navigateToMainScreen() return } } Выводы по кейсам вложенной навигации
Через неделю, в заключительной статье о Navigation Component, я расскажу, как можно организовать навигацию в многомодульных приложениях и как работается с дип линками, а также покажу кейсы со встраиваемыми фрагментами и диалогами. =========== Источник: habr.com =========== Похожие новости:
Блог компании HeadHunter ), #_razrabotka_pod_android ( Разработка под Android ) |
|
Вы не можете начинать темы
Вы не можете отвечать на сообщения
Вы не можете редактировать свои сообщения
Вы не можете удалять свои сообщения
Вы не можете голосовать в опросах
Вы не можете прикреплять файлы к сообщениям
Вы не можете скачивать файлы
Вы не можете отвечать на сообщения
Вы не можете редактировать свои сообщения
Вы не можете удалять свои сообщения
Вы не можете голосовать в опросах
Вы не можете прикреплять файлы к сообщениям
Вы не можете скачивать файлы
Текущее время: 23-Ноя 00:09
Часовой пояс: UTC + 5