Android Jetpack Navigation with ViewPager and TabLayout

前端 未结 5 804
面向向阳花
面向向阳花 2020-12-13 10:09

For a new app i use Jetpack Navigation Library to implement proper back navigation. The first level of navigation is a navigation drawer which works fine with jetpack naviga

相关标签:
5条回答
  • 2020-12-13 10:16

    What worked for me so far:

    In navigation_graph.xml

    • make your ViewPagerFragment the root of a nested graph
    • connect your in and out navigation to the nested graph

    in nested graph:

    • add the ChildFragments of the ViewPager to the nested graph

    I didnt need to change the ViewPager, and Directions were created for the Child Fragments so navigation is possible from there.

    0 讨论(0)
  • 2020-12-13 10:21

    This worked for me. I added the viewPagerTabs fragment to nested graph like so:

    <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>
    

    and then inside the child fragment of the viewpager:

    val action = TabsFragmentDirections.actionNavTabsToNavSend()
    findNavController().navigate(action)
    
    0 讨论(0)
  • 2020-12-13 10:23

    How you implement appbar navigation changes your implementation. If you wish to use navigation from page to detail, it's using same fragmentManager the main NavHost fragment uses. It's like going to detail fragment/activity.

    Home, Dashboard and Notification have their own graphs so they can open their child fragments while Login fragment belongs to main nav graph so it opens it's fragment as detail fragment.

    This implementation requires main NavHostFragment in a fragment or MainActivity.

    Layouts

    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>
    

    As of now androidx.fragment.app.FragmentContainerView crashes with appbar navigation, so use fragment if you encounter navController not found error

    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.

    Navivgation Graphs

    Main nav graph, 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)
        }
    

    in NavHost fragments when fragment's navHost is attached.

    MainActivity

    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 function is just to observe how main fragment stack and fragment change, it has only observational purpose, remove it if not needed.

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

    Fragments with HostFragment have no appbar navigation since it's not implemented in this example.

    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
    
        /*
                                                                                
    0 讨论(0)
  • 2020-12-13 10:35

    Experimented with different approaches to handle TabLayout with Jetpack Navigation. But hit issues like having a full history of switching between tabs multiple times etc.

    Browsing known Google Android Issues before raising a request for a demo, I found this existing issue.

    Its status is Closed marked as Intended Behavior with the following explanation:

    Navigation focuses on elements that affect the back stack and tabs do not affect the back stack - you should continue to manage tabs with a ViewPager and TabLayout - Referring to Youtube training.

    0 讨论(0)
  • 2020-12-13 10:39

    Yes, but you will have to implement your own custom destination, by implementing the class Navigator and overriding at least the methods popBackStack() and navigate().

    In your navigate, you will have to call the ViewPager.setCurrentTab() and add it to your back stack. Something like:

    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
    }
    

    In your popBackStack, you will have to set back the last item selected. Something like:

    override fun popBackStack(): Boolean {
        if(backstack.size() <= 1) return false
    
        viewPager.currentItem = backstack.peekLast()
        backstack.removeLast()
    
        return true
    }
    

    You can find a brief explanation on Android docs and this example of custom navigator for FragmentDialog.

    After implementing your ViewPagerNavigator, you will have to add it to your NavController and set the listeners of tab views selection to call NavController.navigate().

    I hope someone will implement a library for all this common patterns (ViewPager, ViewGroup, FragmentDialog), if anyone find it, put it on the comments.

    0 讨论(0)
提交回复
热议问题