Вы сейчас в четвертой части большого материала про Navigation Component в многомодульном проекте. Если вы уже знаете:
То добро пожаловать в заключительную часть истории о моем опыте с этой прекрасной библиотекой — про решение для iOS-like multistack-навигации.
Посмотреть как всё это работает можно тут
Если не знаете, то выйдите и зайдите нормально прочитайте сначала три статьи выше.
В дополнение к библиотеке Navigation Component Google выпустили несколько интерфейсных дополнений под названием NavigationUI, которые помогут вам подключить навигацию к BottomBar, Menu и прочим стандартным компонентам. Но часто поступают требования, чтобы на каждой вкладке был свой стек и текущие состояния сохранялись при переходе между ними. К сожалению, из коробки Navigation Component и NavigationUI так не умеют.
Поддержку такого подхода представили сами Google в своем architecture-components-samples репозитории на GitHub (https://github.com/android/architecture-components-samples/tree/master/NavigationAdvancedSample). Суть его проста:
Добавляем FragmentContainer.
Создаем NavHostFragment и граф под каждую вкладку.
При выборе вкладки присоединяем необходимый NavHostFragment и отсоединяем текущий с помощью транзакций FragmentManager-a.
Но в ходе работы с этим решением я переделал некоторые моменты, связанные со спецификой проекта:
Многие приложения имеют sign in / up flow, on boarding и прочие экраны, которые не должны входить в стеки, но даже в таком случае все достаточно просто оборачивается стандартными средствами. Навигацию между этими частями можно выстроить уже как обычно, например, как в предыдущей части.
В примере все стеки инициализируются сразу при старте приложения. Связано это с корректной работой NavigationBottomBar и обработкой Deep Link-ов. Но я часто сталкивался с проектами, где deep link-и не нужны и бар навигации требует кастомизации. Проект, на котором я обкатывал подход — не исключение. Глядя на оригинальный файл NavigationExtensions в 250 loc, я решил выбросить все ненужное и сделать lazy-инициализацию NavHost-ов, оставив только основные функции:
Функция поиска / инициализации требуемого NavHost-фрагмента:
fun obtainNavHostFragment( fragmentManager: FragmentManager, fragmentTag: String, navGraphId: Int, containerId: Int ): NavHostFragment { // If the Nav Host fragment exists, return it val existingFragment = fragmentManager.findFragmentByTag(fragmentTag) as NavHostFragment? existingFragment?.let { return it } // Otherwise, create it and return it. val navHostFragment = NavHostFragment.create(navGraphId) fragmentManager.beginTransaction() .add(containerId, navHostFragment, fragmentTag) .commitNow() return navHostFragment }
Функция смены NavHost-ов:
protected fun selectTab(tab: Tab) { val newFragment = obtainNavHostFragment( childFragmentManager, getFragmentTag(tabs.indexOf(tab)), tab.graphId, containerId ) val fTrans = childFragmentManager.beginTransaction() with(fTrans) { if (selectedFragment != null) detach(selectedFragment!!) attach(newFragment) commitNow() } selectedFragment = newFragment currentNavController = selectedFragment!!.navController tabSelected(tab) }
Функция своей обработки нажатия на кнопку “Back”:
activity?.onBackPressedDispatcher?.addCallback( viewLifecycleOwner, object: OnBackPressedCallback(true){ override fun handleOnBackPressed() { val isNavigatedUp = currentNavController.navigateUp() if(isNavigatedUp){ return }else{ activity?.finish() } } } )
В итоге
Таким образом мы получаем iOS-like навигацию и даже лучше, так как имеем lazy-нагрузку на стеках и меньшее количество кода. Приятный бонус — мы имеем полностью очевидную и прозрачную схему навигации, которую просто масштабировать и модифицировать.
