使用ViewPager和TabLayout的Android Jetpack导航

ngu*_*ngu 19 android android-navigation android-architecture-components android-jetpack android-architecture-navigation

对于新的应用程序,我使用Jetpack导航库来实现正确的后退导航.第一级导航是导航抽屉,如文档中所述,可以使用jetpack导航.但是ViewPager和TabLayout实现了另一个导航级别.由TabLayout切换的片段包含额外的线性导航层次结构.但是,在Jetpack Navigation中似乎不支持ViewPager/TabLayout.必须实现FragmentPagerAdapter,并在切换选项卡时结束托管后端堆栈.顶级导航与每个选项卡内的导航之间存在脱节.有没有办法让这个工作与Jetpack导航?

sun*_*rer 9

实验了TabLayout使用Jetpack Navigation处理不同方法的方法。但遇到一些麻烦的问题,例如具有在选项卡之间多次切换的完整历史等。

在提出演示请求之前浏览了已知的Google Android问题,我发现了这个现有问题

其状态为已关闭,标记为预期行为,其解释如下:

导航集中在影响后堆栈的元素上,而选项卡不影响后堆栈-您应继续使用ViewPager和来管理选项卡TabLayout。-参考Youtube培训


Thr*_*ian 6

您实现应用栏导航的方式会改变您的实现。如果您希望使用从页面到详细信息的导航,它使用与主要 NavHost 片段相同的 fragmentManager。这就像详细介绍片段/活动。

在此处输入图片说明

主页、仪表板和通知有自己的图表,因此它们可以打开子片段,而登录片段属于主导航图,因此它将片段作为详细信息片段打开。

此实现需要NavHostFragment在片段或MainActivity.

布局

活动_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>
Run Code Online (Sandbox Code Playgroud)

截至目前androidx.fragment.app.FragmentContainerView崩溃,并appbar导航,所以使用的片段,如果你遇到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>
Run Code Online (Sandbox Code Playgroud)

ViewPager2 的 Fragment 有 NavHostFragment,只添加一个,其他的和这个布局一样,除了app:navGraph="@navigation/nav_graph_home"自己的图。

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>
Run Code Online (Sandbox Code Playgroud)

其他片段没有什么特别之处,跳过它们,如果您有兴趣,我添加了完整示例和其他导航组件示例的链接。

导航图

主导航图,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" />
Run Code Online (Sandbox Code Playgroud)

ViewPager2的页面导航图之一,其他相同。

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" />
Run Code Online (Sandbox Code Playgroud)

ViewPager 导航图的重要事项是在屏幕上使用片段而不是 NavHost 片段,否则您需要设置导航

  if (navController!!.currentDestination == null || navController!!.currentDestination!!.id == navController!!.graph.startDestination) {
        navController?.navigate(R.id.homeFragment1)
    }
Run Code Online (Sandbox Code Playgroud)

当附加片段的 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()
        }
    }
}
Run Code Online (Sandbox Code Playgroud)

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()
        }
    }

}
Run Code Online (Sandbox Code Playgroud)

带有 HostFragment 的 Fragment 没有应用栏导航,因为它没有在这个例子中实现。

主片段

类 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
Run Code Online (Sandbox Code Playgroud)

}

MainFragment 设置选项卡,BaseDataBindingFragment仅使用数据绑定通过getLayoutRes()

最后 Pager 的嵌套片段

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()
            }
        }
    }

}
Run Code Online (Sandbox Code Playgroud)

这里重要的是正确使用 onBackPressedDispatcher 。ViewPager2 中嵌套片段返回导航存在一些问题。

  1. 由于按下后退按钮时片段不会添加到主后退堆栈中,因此 Activity 会完全跳过 ViewPager 后退堆栈。为了解决这个问题,你应该使用OnBackPressedCallbacknavController?.navigateUp()
  2. 例如,当您OnBackPressedCallback在 ViewPager 片段的根目录下使用时HomeFragment1,您无法返回,因为您正在使用navController?.navigateUp(). 要修复它,您应该检查if (navController?.currentDestination?.id == navController?.graph?.startDestination) 是根。
  3. 当你打电话给requireActivity().onBackPressed()它时handleOnBackPressed,它会创建一个无限循环。所以,之前禁用回调并再次重置它。
  4. 当您的片段不可见时,还要在 onPause() 中禁用回调,以防止在调用其他片段时handleOnBackPressed调用它

我创建了其他示例,包括为 ViewPager2 的子片段提供嵌套导航的示例,这是当前项目链接。对于下图的那个。更棘手的是需要使用 LiveData 并且有旋转问题。还要添加另一个使用 ViewModel 解决此问题的示例。

在此处输入图片说明