Для нового приложения я использую библиотеку навигации Jetpack, чтобы реализовать правильную обратную навигацию. Первый уровень навигации - это панель навигации, которая отлично работает с навигацией на реактивном ранце, как описано в документации. Но есть еще один уровень навигации, реализованный с помощью ViewPager и TabLayout. Фрагменты, переключенные TabLayout, содержат дополнительную линейную иерархию навигации. Однако, похоже, нет поддержки ViewPager / TabLayout в Jetpack Navigation. Должен быть реализован FragmentPagerAdapter, и управляемый задний стек завершается при переключении вкладок. Существует разрыв между навигацией верхнего уровня и навигацией внутри каждой вкладки. Есть ли способ заставить это работать с Jetpack Navigation?
Навигация в Android Jetpack с помощью ViewPager и TabLayout
Ответы (5)
Экспериментировал с различными подходами к управлению TabLayout
с помощью Jetpack Navigation. Но есть проблемы, такие как наличие полной истории переключения между вкладками несколько раз и т. Д.
Просматривая известные проблемы Google Android, прежде чем запросить демонстрацию, я обнаружил эту существующую проблему.
Его статус Закрыт помечен как Предполагаемое поведение со следующим объяснением:
Навигация сосредоточена на элементах, которые влияют на задний стек, а вкладки не влияют на задний стек - вам следует продолжать управлять вкладками с помощью
ViewPager
иTabLayout
- см. Обучение на YouTube.
То, как вы реализуете навигацию на панели приложений, меняет вашу реализацию. Если вы хотите использовать навигацию от страницы к деталям, используйте тот же fragmentManager, что и основной фрагмент NavHost. Это все равно что детализировать фрагмент / действие.
Home, Dashboard и Notification имеют свои собственные графики, поэтому они могут открывать свои дочерние фрагменты, в то время как фрагмент входа принадлежит основному графу навигации, поэтому он открывает его как фрагмент детали.
Для этой реализации требуется main NavHostFragment
во фрагменте или MainActivity
.
Макеты
activity_main.xml
<?xml version="1.0" encoding="utf-8"?>
<layout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto">
<androidx.coordinatorlayout.widget.CoordinatorLayout
android:layout_width="match_parent"
android:layout_height="match_parent">
<com.google.android.material.appbar.AppBarLayout
android:id="@+id/appbar"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:theme="@style/ThemeOverlay.AppCompat.Dark.ActionBar">
<androidx.appcompat.widget.Toolbar
android:id="@+id/toolbar"
android:layout_width="match_parent"
android:layout_height="?attr/actionBarSize"
app:popupTheme="@style/ThemeOverlay.AppCompat.ActionBar" />
</com.google.android.material.appbar.AppBarLayout>
<androidx.constraintlayout.widget.ConstraintLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
app:layout_behavior="@string/appbar_scrolling_view_behavior">
<fragment
android:id="@+id/nav_host_fragment"
android:name="androidx.navigation.fragment.NavHostFragment"
android:layout_width="0dp"
android:layout_height="0dp"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintRight_toRightOf="parent"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintBottom_toBottomOf="parent"
app:defaultNavHost="true"
app:navGraph="@navigation/nav_graph"/>
</androidx.constraintlayout.widget.ConstraintLayout>
</androidx.coordinatorlayout.widget.CoordinatorLayout>
</layout>
На данный момент androidx.fragment.app.FragmentContainerView дает сбой при навигации по панели приложений, поэтому используйте фрагмент, если вы столкнулись с ошибкой navController
не найден.
fragment_main.xml
<androidx.constraintlayout.widget.ConstraintLayout
android:layout_width="match_parent"
android:layout_height="match_parent">
<com.google.android.material.tabs.TabLayout
android:id="@+id/tabLayout"
android:layout_width="match_parent"
android:background="@color/colorPrimary"
app:tabTextColor="#fff"
android:layout_height="wrap_content"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
app:tabMode="scrollable" />
<androidx.viewpager2.widget.ViewPager2
android:id="@+id/viewPager"
android:layout_width="match_parent"
android:layout_height="0dp"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/tabLayout" />
</androidx.constraintlayout.widget.ConstraintLayout>
Fragments for ViewPager2 that have NavHostFragment, only add one, others have the same layout as this one except app:navGraph="@navigation/nav_graph_home"
with their own graphs.
fragment_nav_host_home.xml
<androidx.constraintlayout.widget.ConstraintLayout
android:layout_width="match_parent"
android:layout_height="match_parent">
<androidx.fragment.app.FragmentContainerView
android:id="@+id/nested_nav_host_fragment_home"
android:name="androidx.navigation.fragment.NavHostFragment"
android:layout_width="0dp"
android:layout_height="0dp"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintRight_toRightOf="parent"
app:layout_constraintTop_toTopOf="parent"
app:defaultNavHost="false"
app:navGraph="@navigation/nav_graph_home" />
</androidx.constraintlayout.widget.ConstraintLayout>
Nothing special with other fragments, skipped them, i added link for full sample and other navigation component examples if you are interested.
Графики навигации
Главный навигационный график, nav_graph.xml
<!-- MainFragment-->
<fragment
android:id="@+id/main_dest"
android:name="com.smarttoolfactory.tutorial6_2navigationui_viewpager2_nestednavhost.blankfragment.MainFragment"
android:label="MainFragment"
tools:layout="@layout/fragment_main">
<!-- Login -->
<action
android:id="@+id/action_main_dest_to_loginFragment2"
app:destination="@id/loginFragment2" />
</fragment>
<!-- Global Action Start -->
<action
android:id="@+id/action_global_start"
app:destination="@id/main_dest"
app:popUpTo="@id/main_dest"
app:popUpToInclusive="true" />
<!-- Login -->
<fragment
android:id="@+id/loginFragment2"
android:name="com.smarttoolfactory.tutorial6_2navigationui_viewpager2_nestednavhost.blankfragment.LoginFragment2"
android:label="LoginFragment2" />
And one of the nav graph for pages of ViewPager2, others are same.
nav_graph_home.xml
<fragment
android:id="@+id/home_dest"
android:name="com.smarttoolfactory.tutorial6_2navigationui_viewpager2_nestednavhost.navhost.HomeNavHostFragment"
android:label="HomeHost"
tools:layout="@layout/fragment_navhost_home" />
<fragment
android:id="@+id/homeFragment1"
android:name="com.smarttoolfactory.tutorial6_2navigationui_viewpager2_nestednavhost.blankfragment.HomeFragment1"
android:label="HomeFragment1"
tools:layout="@layout/fragment_home1">
<action
android:id="@+id/action_homeFragment1_to_homeFragment2"
app:destination="@id/homeFragment2" />
</fragment>
<fragment
android:id="@+id/homeFragment2"
android:name="com.smarttoolfactory.tutorial6_2navigationui_viewpager2_nestednavhost.blankfragment.HomeFragment2"
android:label="HomeFragment2"
tools:layout="@layout/fragment_home2">
<action
android:id="@+id/action_homeFragment2_to_homeFragment3"
app:destination="@id/homeFragment3" />
</fragment>
<fragment
android:id="@+id/homeFragment3"
android:name="com.smarttoolfactory.tutorial6_2navigationui_viewpager2_nestednavhost.blankfragment.HomeFragment3"
android:label="HomeFragment3"
tools:layout="@layout/fragment_home3" />
Important thing with ViewPager nav graphs is to use fragment on screen instead of NavHost fragment, you need otherwise set navigation with
if (navController!!.currentDestination == null || navController!!.currentDestination!!.id == navController!!.graph.startDestination) {
navController?.navigate(R.id.homeFragment1)
}
во фрагментах NavHost, когда прикреплен navHost фрагмента.
Основная деятельность
class MainActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
listenBackStackChange()
}
private fun listenBackStackChange() {
// Get NavHostFragment
val navHostFragment =
supportFragmentManager.findFragmentById(R.id.main_nav_host_fragment)
// ChildFragmentManager of NavHostFragment
val navHostChildFragmentManager = navHostFragment?.childFragmentManager
navHostChildFragmentManager?.addOnBackStackChangedListener {
val backStackEntryCount = navHostChildFragmentManager.backStackEntryCount
val fragments = navHostChildFragmentManager.fragments
Toast.makeText(
this,
"Main graph backStackEntryCount: $backStackEntryCount, fragments: $fragments",
Toast.LENGTH_SHORT
).show()
}
}
}
listenBackStackChange
Функция предназначена только для наблюдения за изменением основного стека и фрагмента, она имеет только наблюдательную цель, удаляет ее, если не требуется.
Адаптер для ViewPager2
class ChildFragmentStateAdapter(private val fragment: Fragment) :
FragmentStateAdapter(fragment) {
override fun getItemCount(): Int = 4
override fun createFragment(position: Int): Fragment {
return when (position) {
0 -> HomeNavHostFragment()
1 -> DashBoardNavHostFragment()
2 -> NotificationHostFragment()
else -> LoginFragment1()
}
}
}
Фрагменты с HostFragment не имеют навигации на панели приложений, поскольку в этом примере это не реализовано.
MainFragment
class MainFragment: BaseDataBindingFragment () {
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
// TabLayout
val tabLayout = dataBinding.tabLayout
// ViewPager2
val viewPager = dataBinding.viewPager
/*
???? Set Adapter for ViewPager inside this fragment using this Fragment,
more specifically childFragmentManager as param
*/
viewPager.adapter = ChildFragmentStateAdapter(this)
// Bind tabs and viewpager
TabLayoutMediator(tabLayout, viewPager) { tab, position ->
when(position) {
0-> tab.text = "Home"
1-> tab.text = "Notification"
2-> tab.text = "Dashboard"
3-> tab.text = "Login"
}
}.attach()
}
override fun getLayoutRes(): Int = R.layout.fragment_main
}
MainFragment устанавливает вкладки, BaseDataBindingFragment
использует только привязку данных через getLayoutRes()
Наконец, вложенные фрагменты Пейджера
class HomeNavHostFragment : BaseDataBindingFragment<FragmentNavhostHomeBinding>() {
override fun getLayoutRes(): Int = R.layout.fragment_navhost_home
var navController: NavController? = null
private val nestedNavHostFragmentId = R.id.nested_nav_host_fragment_home
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
/*
???? This is navController we get from findNavController not the one required
for navigating nested fragments
*/
val mainNavController =
Navigation.findNavController(requireActivity(), R.id.nav_host_fragment)
val nestedNavHostFragment =
childFragmentManager.findFragmentById(nestedNavHostFragmentId) as? NavHostFragment
navController = nestedNavHostFragment?.navController
/*
???? Alternative 1
Navigate to HomeFragment1 if there is no current destination and current destination
is start destination. Set start destination as this fragment so it needs to
navigate next destination.
If start destination is NavHostFragment it's required to navigate to first
*/
// if (navController!!.currentDestination == null || navController!!.currentDestination!!.id == navController!!.graph.startDestination) {
// navController?.navigate(R.id.homeFragment1)
// }
/*
???? Alternative 2 Reset graph to default status every time this fragment's view is created
❌ This does not work if initial destination if this fragment because it repeats
creating this fragment in an infinite loop since graph is created every time
*/
// val navInflater = navController!!.navInflater
// nestedNavHostFragment!!.navController.graph = graph
// val graph = navController!!.navInflater.inflate(navGraphId)
// nestedNavHostFragment!!.navController.graph = graph
// Listen on back press
listenOnBackPressed()
}
private fun listenOnBackPressed() {
requireActivity().onBackPressedDispatcher.addCallback(viewLifecycleOwner, callback)
}
override fun onResume() {
super.onResume()
callback.isEnabled = true
}
override fun onPause() {
super.onPause()
callback.isEnabled = false
}
// This should be false, true causes problems on rotation
val callback = object : OnBackPressedCallback(false) {
override fun handleOnBackPressed() {
// Get NavHostFragment
val navHostFragment =
childFragmentManager.findFragmentById(nestedNavHostFragmentId)
// ChildFragmentManager of the current NavHostFragment
val navHostChildFragmentManager = navHostFragment?.childFragmentManager
val currentDestination = navController?.currentDestination
val backStackEntryCount = navHostChildFragmentManager!!.backStackEntryCount
val isAtStartDestination =
(navController?.currentDestination?.id == navController?.graph?.startDestination)
// Check if it's the root of nested fragments in this navhost
if (navController?.currentDestination?.id == navController?.graph?.startDestination) {
/*
Disable this callback because calls OnBackPressedDispatcher
gets invoked calls this callback gets stuck in a loop
*/
isEnabled = false
requireActivity().onBackPressed()
isEnabled = true
} else {
navController?.navigateUp()
}
}
}
}
Здесь важно правильно использовать onBackPressedDispatcher. Есть некоторые проблемы с обратной навигацией по вложенным фрагментам в ViewPager2.
- Поскольку фрагменты не добавляются в основной задний стек при нажатии кнопки возврата, Activity полностью пропускает задний стек ViewPager. Чтобы решить эту проблему, вы должны использовать
OnBackPressedCallback
сnavController?.navigateUp()
- Когда вы используете
OnBackPressedCallback
, когда находитесь в корне фрагмента ViewPager, напримерHomeFragment1
, вы не можете вернуться, поскольку используетеnavController?.navigateUp()
. Чтобы исправить это, вы должны проверить, чтоif (navController?.currentDestination?.id == navController?.graph?.startDestination)
- это корень. - Когда вы вызываете
requireActivity().onBackPressed()
, он вызываетhandleOnBackPressed
и создает бесконечный цикл. Итак, отключите обратный вызов раньше и снова сбросьте его. - Также отключите обратный вызов в onPause (), когда ваш фрагмент не виден, чтобы предотвратить его вызов при вызове
handleOnBackPressed
других фрагментов
Я создал другие примеры, включая пример с вложенной навигацией для дочерних фрагментов ViewPager2, это ссылка на текущий проект. Для с изображением ниже. Это более сложно, требует использования LiveData и имеет проблемы с вращением. Также добавьте еще один пример с ViewModel, который также решает эту проблему.
Что сработало для меня до сих пор:
В navigation_graph.xml
- сделайте ViewPagerFragment корнем вложенного графа
- подключите вашу входящую и выходящую навигацию к вложенному графу
во вложенном графе:
- добавить ChildFragments ViewPager во вложенный граф
Мне не нужно было менять ViewPager, и для дочерних фрагментов были созданы направления, поэтому оттуда возможна навигация.
Это сработало для меня. Я добавил фрагмент viewPagerTabs во вложенный граф следующим образом:
<navigation
android:id="@+id/nav_nested_graph"
app:startDestination="@id/nav_viewpager_tab">
<fragment
android:id="@+id/nav_pager_tab"
android:name="com.android.ui.tabs.TabsFragment"
android:label="@string/tag_tabs"
tools:layout="@layout/tabs_fragment">
<action
android:id="@+id/action_nav_tabs_to_nav_send"
app:destination="@id/nav_send_graph">
</fragment>
</navigation>
а затем внутри дочернего фрагмента окна просмотра:
val action = TabsFragmentDirections.actionNavTabsToNavSend()
findNavController().navigate(action)
Да, но вам нужно будет реализовать свое собственное назначение, реализовав класс _ 1_ и переопределение как минимум методов popBackStack()
и _ 3_.
В вашем navigate
вам нужно будет вызвать ViewPager.setCurrentTab()
и добавить его в свой задний стек. Что-то вроде:
lateinit var viewPager: ViewPager? = null // you have to provide this in the constructor
private val backstack: Deque<Pair<Int, View>> = ArrayDeque
override fun navigate(destination: Destination, args: Bundle?,
navOptions: NavOptions?, navigatorExtras: Extras?
): NavDestination? {
viewPager.currentItem = destination.id
backstack.remove(destination.id) // remove so the stack has never two of the same
backstack.addLast(destination.id)
return destination
}
В вашем popBackStack
вам нужно будет вернуть последний выбранный элемент. Что-то вроде:
override fun popBackStack(): Boolean {
if(backstack.size() <= 1) return false
viewPager.currentItem = backstack.peekLast()
backstack.removeLast()
return true
}
Краткое объяснение можно найти в документации по Android и этот пример настраиваемого навигатора для FragmentDialog
.
После реализации вашего ViewPagerNavigator
вам нужно будет добавить его в свой NavController
и настроить прослушиватели выбора представлений вкладок на вызов NavController.navigate()
.
Надеюсь, кто-нибудь реализует библиотеку для всех этих распространенных паттернов (ViewPager, ViewGroup, FragmentDialog), если кто-нибудь найдет, выложите в комментарии.