Вы сейчас в четвертой части большого материала про 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-нагрузку на стеках и меньшее количество кода. Приятный бонус — мы имеем полностью очевидную и прозрачную схему навигации, которую просто масштабировать и модифицировать.