How to implement a ViewPager with BottomNavigationView using new Navigation Architecture Component?
Asked Answered
C

9

47

I have an application with a BottomNavigationView and ViewPager. How is it possible to implement it using new "Navigation Architecture Component?"

What is the best practice?

Thanks so much

Clupeoid answered 3/10, 2018 at 15:12 Comment(1)
You can't because viewpager has a different back stack! You can, on the other hand, have a separate nested_graph and from within the view_pager Fragment navigate into that nested_graph.Pohai
M
42

UPDATE (15/06/21):

Starting from Navigation component version 2.4.0-alpha01 multiple back stacks are supported out of the box. According to documentation if you are using NavigationView or BottomNavigationView together with Navigation component, then multiple back stacks should work without any code changes to previous implementation.

As part of this change, the NavigationUI methods of onNavDestinationSelected(), BottomNavigationView.setupWithNavController() and NavigationView.setupWithNavController() now automatically save and restore the state of popped destinations, enabling support for multiple back stacks without any code changes. When using Navigation with Fragments, this is the recommended way to integrate with multiple back stacks.

Original Answer:

Default implementation of BottomNavigationView with Navigation Arch Component didn't work out for me. When clicking on tabs it starts them from beginning according to navigation graph.

I need to have 5 tabs in the bottom of the screen and have a separate back stack for each of the tabs. Which means when switching between tabs you will always return to the exactly the same state as it was before leaving (like in Instagram).

My approach is as follows:

  1. Put ViewPager and BottomNavigationView in activity_main.xml
  2. Set OnNavigationItemSelectedListener to BottomNavigationView in MainActivity.kt
  3. Create separate Container fragments for each of the tabs (they will be the starting point of each tab)
  4. include NavHostFragment inside of Container fragments' xml.
  5. Implement necessary code for Navigation Arch Component in each of the Container fragments.
  6. Create a graph for each of the tabs

Note: each of the graphs can interact with each other.

Important point here is that we place Toolbar not in activity but in Container fragment. Then we call setupWithNavController() on toolbar itself without setting it as supportActionBar. This way toolbar titles will be automatically updated and Back/Up button will be managed automatically.

Results:

  • ViewPager stored states of each tabs.
  • Didn't worry about fragment transactions.
  • SafeArgs and DeepLinking works as expected.
  • We have full control over BottomNavigationManager and ViewPager (i.e. we can implement OnNavigationItemReselectedListener and decide to scroll lists in current tab to top before popping back stack).

Code:

activity_main.xml

<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:orientation="vertical"
    tools:context=".MainActivity">

    <androidx.viewpager.widget.ViewPager
        android:id="@+id/main_view_pager"
        android:layout_width="match_parent"
        android:layout_height="0dp"
        android:layout_weight="1" />

    <com.google.android.material.bottomnavigation.BottomNavigationView
        android:id="@+id/main_bottom_navigation_view"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:background="?android:attr/windowBackground"
        app:menu="@menu/navigation" />

</LinearLayout>

MainActivity.kt

import kotlinx.android.synthetic.main.activity_main.*

class MainActivity : AppCompatActivity() {

    private lateinit var viewPagerAdapter: ViewPagerAdapter

    private val mOnNavigationItemSelectedListener = BottomNavigationView.OnNavigationItemSelectedListener { item ->
        when (item.itemId) {
            R.id.navigation_tab_1 -> {
                main_view_pager.currentItem = 0
                return@OnNavigationItemSelectedListener true
            }
            R.id.navigation_tab_2 -> {
                main_view_pager.currentItem = 1
                return@OnNavigationItemSelectedListener true
            }
        }
        false
    }

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)

        viewPagerAdapter = ViewPagerAdapter(supportFragmentManager)
        main_view_pager.adapter = viewPagerAdapter
        
        main_bottom_navigation_view.setOnNavigationItemSelectedListener(mOnNavigationItemSelectedListener)
    }
}

ViewPagerAdapter.kt

class ViewPagerAdapter(fm: FragmentManager) : FragmentPagerAdapter(fm) {

    override fun getItem(position: Int): Fragment {
        return when (position) {
            0 -> Tab1ContainerFragment()
            else -> Tab2ContainerFragment()
        }
    }

    override fun getCount(): Int {
        return 2
    }
}

fragment_tab_1_container.xml

<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    tools:context=".Tab1ContainerFragment">

    <androidx.appcompat.widget.Toolbar
        android:id="@+id/tab_1_toolbar"
        android:layout_width="match_parent"
        android:layout_height="?attr/actionBarSize"
        android:background="@color/colorPrimary"
        android:theme="@style/ThemeOverlay.AppCompat.Dark" />

    <fragment
        android:id="@+id/tab_1_nav_host_fragment"
        android:name="androidx.navigation.fragment.NavHostFragment"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        app:defaultNavHost="true"
        app:navGraph="@navigation/navigation_graph_tab_1" />

</RelativeLayout>

Tab1ContainerFragment.kt

class Tab1ContainerFragment : Fragment() {

    override fun onCreateView(
        inflater: LayoutInflater, container: ViewGroup?,
        savedInstanceState: Bundle?
    ): View? {
        return inflater.inflate(R.layout.fragment_tab_1_container, container, false)
    }

    override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
        super.onViewCreated(view, savedInstanceState)

        val toolbar = view.findViewById<Toolbar>(R.id.tab_1_toolbar)

        val navHostFragment = childFragmentManager.findFragmentById(R.id.tab_1_nav_host_fragment) as NavHostFragment? ?: return

        val navController = navHostFragment.navController

        val appBarConfig = AppBarConfiguration(navController.graph)

        toolbar.setupWithNavController(navController, appBarConfig)
    }
}

We can create as many navigation graphs as you want:

navigation graphs

But we need to have a separate graph for each tabs:

<navigation xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:tools="http://schemas.android.com/tools"
    android:id="@+id/navigation_graph_tab_1"
    app:startDestination="@id/tab1StartFragment">

    <fragment
        android:id="@+id/tab1StartFragment"
        android:name="com.marat.android.bottomnavigationtutorial.Tab1StartFragment"
        android:label="fragment_tab_1_start"
        tools:layout="@layout/fragment_tab_1_start">
        <action
            android:id="@+id/action_tab_1_to_content"
            app:destination="@id/navigation_graph_content" />
    </fragment>

    <include app:graph="@navigation/navigation_graph_content" />
</navigation>

Here start destination fragment is any fragment you want to appear as first screen in tab.

Mroz answered 24/1, 2019 at 21:36 Comment(7)
I have added all files as mentioned above and TabContainerFragment successfully showing my TabStart fragment. But I am facing issue after including content graph. I am getting crash on below code to navigate from container fragment to content fragment. val navigateTOHome2 = Home1FragmentDirections.actionHome1FragmentToHome2Fragment(); findNavController().navigate(navigateTOHome2) (not found by NavController()) But it's working fine if I am moving my nested navigation code to container navigation. In case when it's working, back popping not working on pressing back button.Velodrome
It is hard to understand the problem without more informations. But you shouldn't navigate from container fragment to content fragment. Instead, code you provided should be placed inside of TabStartFragment.Mroz
When activity is hosting ViewPager directly and tab1Container hosting NavHostFragment then with app:defaultNavHost="true" device back button is not intercepted. What to do?Velodrome
@MuhammadMaqsood this approach works perfectly: https://mcmap.net/q/102112/-handling-back-button-in-android-navigation-componentLunik
I am trying to follow this solution, but I have some trouble with options menu. I have 2 tabs and each of their starting fragments have menus (setHasOptionsMenu(true)). The toolbar / action bar should only show the menu of the fragment that's currently visible in the tab, but this is not the case. The menus from both fragments are shown at the same time, even for the fragment that is not yet visible. I've been trying to figure out how to solve this but I'm running out of ideas now. How solution/work-around for this?Millihenry
@Mroz I'm using the same approach in my app but now I have a question: Do you know how to navigate between the viewpager and the specific fragments in the graph using actions?Inadvertence
@Mroz The issue with this implementation is that when on a tab, you navigate and popUpTo it creates everytime a new instance but it shouldn't ! How to solve ?Bravery
R
8

I created a sample that has Toolbar on Activity, you can also create ViewPager fragments that have their own toolbar. It uses OnBackPressedCallback for back navigation, ViewModel for setting current NavController and NavHostFragment with childFragmentManager or nested fragments, and respects life cycle with viewLifeCycleOwner and disabling callback on pause and enabling onResume.

BottomNavigationView with ViewPager and Navigation Architecture

Navigation and layout architecture

     MainActivity(Appbar + Toolbar  + ViewPager2 + BottomNavigationView)
       |
       |- HomeNavHostFragment
       |  |- HF1 -> HF2 -> HF3
       |
       |- DashboardNavHostFragment
       |  |- DF1 -> DF2 -> DF3
       |
       |- NotificationHostFragment
          |- NF1 -> NF2 -> NF3

First, create a navigation graph for each tab or fragment of ViewPager2

nav_graph_home.xml

<?xml version="1.0" encoding="utf-8"?>
<navigation xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:tools="http://schemas.android.com/tools"
    android:id="@+id/nav_graph_dashboard"
    app:startDestination="@id/dashboardFragment1">


    <fragment
        android:id="@+id/dashboardFragment1"
        android:name="com.smarttoolfactory.tutorial7_1bnw_viewpager2_nestednavigation.blankfragment.DashboardFragment1"
        android:label="DashboardFragment1"
        tools:layout="@layout/fragment_dashboard1">
        <action
            android:id="@+id/action_dashboardFragment1_to_dashboardFragment2"
            app:destination="@id/dashboardFragment2" />
    </fragment>

    <fragment
        android:id="@+id/dashboardFragment2"
        android:name="com.smarttoolfactory.tutorial7_1bnw_viewpager2_nestednavigation.blankfragment.DashboardFragment2"
        android:label="DashboardFragment2"
        tools:layout="@layout/fragment_dashboard2">
        <action
            android:id="@+id/action_dashboardFragment2_to_dashboardFragment3"
            app:destination="@id/dashboardFragment3" />
    </fragment>
    <fragment
        android:id="@+id/dashboardFragment3"
        android:name="com.smarttoolfactory.tutorial7_1bnw_viewpager2_nestednavigation.blankfragment.DashboardFragment3"
        android:label="DashboardFragment3"
        tools:layout="@layout/fragment_dashboard3" >
        <action
            android:id="@+id/action_dashboardFragment3_to_dashboardFragment1"
            app:destination="@id/dashboardFragment1"
            app:popUpTo="@id/dashboardFragment1"
            app:popUpToInclusive="true" />
    </fragment>

</navigation>

other nav graphs are the same as this one

BottomNavigationView's menu

menu_bottom_nav.xml

<?xml version="1.0" encoding="utf-8"?>
<menu xmlns:android="http://schemas.android.com/apk/res/android">

    <item
            android:id="@+id/nav_graph_home"
            android:icon="@drawable/ic_baseline_home_24"
            android:title="Home"/>
    <item
            android:id="@+id/nav_graph_dashboard"
            android:icon="@drawable/ic_baseline_dashboard_24"
            android:title="Dashboard"/>
    <item
            android:id="@+id/nav_graph_notification"
            android:icon="@drawable/ic_baseline_notifications_24"
            android:title="Notification"/>
</menu>

ViewPager2 adapter

class ActivityFragmentStateAdapter(fragmentActivity: FragmentActivity) :
    FragmentStateAdapter(fragmentActivity) {
    
    override fun getItemCount(): Int = 3

    override fun createFragment(position: Int): Fragment {

        return when (position) {
            0 -> HomeNavHostFragment()
            1 -> DashBoardNavHostFragment()
            else -> NotificationHostFragment()
        }
    }
}

Layout for main activity

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

            <androidx.viewpager2.widget.ViewPager2
                android:id="@+id/viewPager"
                android:layout_width="match_parent"
                android:layout_height="0dp"
                app:layout_constraintBottom_toBottomOf="parent"
                app:layout_constraintBottom_toTopOf="@id/bottom_nav"
                app:layout_constraintEnd_toEndOf="parent"
                app:layout_constraintStart_toStartOf="parent"
                app:layout_constraintTop_toTopOf="parent" />


            <com.google.android.material.bottomnavigation.BottomNavigationView
                android:id="@+id/bottom_nav"
                android:layout_width="match_parent"
                android:layout_height="wrap_content"
                app:layout_constraintBottom_toBottomOf="parent"
                app:menu="@menu/menu_bottom_nav" />

        </androidx.constraintlayout.widget.ConstraintLayout>

    </androidx.coordinatorlayout.widget.CoordinatorLayout>

</layout>

MainActivity both listens for BottomNavigationView's item change and change of current NavController when we change tabs, because we have to set Appbar navigation for each tab.

class MainActivity : AppCompatActivity() {

//    private val appbarViewModel by viewModels<AppbarViewModel>()<AppbarViewModel>()

    private val appbarViewModel:AppbarViewModel by viewModels()

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)

        val dataBinding: ActivityMainBinding =
            DataBindingUtil.setContentView(this, R.layout.activity_main)

        val viewPager2 = dataBinding.viewPager
        val bottomNavigationView = dataBinding.bottomNav

        // Cancel ViewPager swipe
        viewPager2.isUserInputEnabled = false

        // Set viewpager adapter
        viewPager2.adapter = ActivityFragmentStateAdapter(this)
        
        // Listen bottom navigation tabs change
        bottomNavigationView.setOnNavigationItemSelectedListener {

            when (it.itemId) {
                R.id.nav_graph_home -> {
                    viewPager2.setCurrentItem(0, false)
                    return@setOnNavigationItemSelectedListener true

                }
                R.id.nav_graph_dashboard -> {
                    viewPager2.setCurrentItem(1, false)
                    return@setOnNavigationItemSelectedListener true
                }
                R.id.nav_graph_notification -> {
                    viewPager2.setCurrentItem(2, false)
                    return@setOnNavigationItemSelectedListener true
                }
            }
            false
        }

        appbarViewModel.currentNavController.observe(this, Observer { navController ->
            navController?.let {
                val appBarConfig = AppBarConfiguration(it.graph)
                dataBinding.toolbar.setupWithNavController(it, appBarConfig)
            }
        })

    }
}

AppbarViewModel has only one MutableLiveData to set current NavController. Purpose of using ViewModel to set ViewModel in NavHost fragments and and being able to get it in Activity or other fragment.

class AppbarViewModel : ViewModel() {
    val currentNavController = MutableLiveData<NavController?>()
}

Layout for NavHost which has FragmentContainerView, when i put Toolbar into these fragments and use FragmentContainerView i get error, use fragment if you use appBar with navigation.

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

</layout>

NavHost Fragment that contains child fragments and NavController, 3 of them are identical so i only put one

class HomeNavHostFragment : BaseDataBindingFragment<FragmentNavhostHomeBinding>() {
    override fun getLayoutRes(): Int = R.layout.fragment_navhost_home

    private val appbarViewModel by activityViewModels<AppbarViewModel>()

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

        val nestedNavHostFragment =
            childFragmentManager.findFragmentById(nestedNavHostFragmentId) as? NavHostFragment
        navController = nestedNavHostFragment?.navController


        // Listen on back press
        listenOnBackPressed()

    }

    private fun listenOnBackPressed() {
        requireActivity().onBackPressedDispatcher.addCallback(viewLifecycleOwner, callback)
    }


    override fun onResume() {
        super.onResume()
        callback.isEnabled = true

        // Set this navController as ViewModel's navController
        appbarViewModel.currentNavController.value = navController

    }

    override fun onPause() {
        super.onPause()
        callback.isEnabled = false
    }

    /**
     * This callback should be created with Disabled because on rotation ViewPager creates
     * NavHost fragments that are not on screen, destroys them afterwards but it might take
     * up to 5 seconds.
     *
     * ### Note: During that interval touching back button sometimes call incorrect [OnBackPressedCallback.handleOnBackPressed] instead of this one if callback is **ENABLED**
     */
    val callback = object : OnBackPressedCallback(false) {

        override fun handleOnBackPressed() {


            // Check if it's the root of nested fragments in this navhost
            if (navController?.currentDestination?.id == navController?.graph?.startDestination) {

                Toast.makeText(requireContext(), "AT START DESTINATION ", Toast.LENGTH_SHORT)
                    .show()

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

        }
    }
}

Important things to be aware with nested navigation are

  1. Being able to navigate properly when back button is pressed
  2. Only navigating from the visible fragment, if not properly implemented other fragment callback back press get invoked
  3. After rotation only setting the visible fragment's back press in active state

First of all, you need to check out if you are the start destination of the graph, because you need to call requireActivity().onBackPressed() to call Activity back or you get stuck at HomeFragment1 for instance

If you don't disable callback before calling requireActivity().onBackPressed() you get stuck in a loop because onBackPressed also calls Active callbacks

If you don't disable callback.isEnabled = false when your current Fragment is not visible every callback gets called

And finally and i think the most important one is if you rotate your device

Fragments in other tabs also get created by viewPager, then destroyed 3 to 5 later, but their onResume is not called, this causes other callbacks to call handleBackPressed if you create object : OnBackPressedCallback(true), use

object : OnBackPressedCallback(false)

For instance if callback is active and you rotate device when HomeFragment3 is open and you touch back button while callback is active

2020-06-28 13:23:42.722 I: 🏠 HomeNavHostFragment #208670033  onCreate()
2020-06-28 13:23:42.729 I: ⏰ NotificationHostFragment #19727909  onCreate()
2020-06-28 13:23:42.826 I: 🏠 HomeNavHostFragment #208670033  onViewCreated()
2020-06-28 13:23:42.947 I: ⏰ NotificationHostFragment #19727909  onViewCreated()
2020-06-28 13:23:42.987 I: 🏠 HomeNavHostFragment #208670033  onResume()
2020-06-28 13:23:44.092 I: ⏰ NotificationHostFragment #19727909 handleOnBackPressed()
2020-06-28 13:23:44.851 I: ⏰ NotificationHostFragment #19727909 handleOnBackPressed()
2020-06-28 13:23:53.011 I: ⏰ NotificationHostFragment #19727909  onDestroyView()
2020-06-28 13:23:53.023 I: ⏰ NotificationHostFragment #19727909  onDestroy()

Even i press back button twice while HomeFragment3 is visible, ⏰ NotificationHostFragment #19727909 handleOnBackPressed() is invoked because ViewPager creates the fragments that are also not visible and destroys them afterwards. It took 10 seconds in my instance, you can also try it out.

EDIT: Instead of onBackPressedDispatcher in each fragment of ViewPager 2, it's advised to use the snipped below in FragmentStateAdapter which sets active fragment on screen as primary navigation fragment.

/**
 * FragmentStateAdapter to add ability to set primary navigation fragment
 * which lets fragment visible to be navigable when back button is pressed using
 * [FragmentStateAdapter.FragmentTransactionCallback] in [ViewPager2].
 *
 * * 🔥 Create FragmentStateAdapter with viewLifeCycleOwner instead of Fragment to make sure
 * that it lives between [Fragment.onCreateView] and [Fragment.onDestroyView] while [View] is alive
 *
 * * https://mcmap.net/q/57492/-leak-canary-detects-memory-leaks-for-tablayout-with-viewpager2
 */
abstract class NavigableFragmentStateAdapter(
    fragmentManager: FragmentManager,
    lifecycle: Lifecycle
) : FragmentStateAdapter(fragmentManager, lifecycle) {

    private val fragmentTransactionCallback =
        object : FragmentStateAdapter.FragmentTransactionCallback() {
            override fun onFragmentMaxLifecyclePreUpdated(
                fragment: Fragment,
                maxLifecycleState: Lifecycle.State
            ) = if (maxLifecycleState == Lifecycle.State.RESUMED) {

                // This fragment is becoming the active Fragment - set it to
                // the primary navigation fragment in the OnPostEventListener
                OnPostEventListener {
                    fragment.parentFragmentManager.commitNow {
                        setPrimaryNavigationFragment(fragment)
                    }
                }
            } else {
                super.onFragmentMaxLifecyclePreUpdated(fragment, maxLifecycleState)
            }
        }

    init {
        // Add a FragmentTransactionCallback to handle changing
        // the primary navigation fragment
        registerFragmentTransactionCallback()
    }

    fun registerFragmentTransactionCallback() {
        registerFragmentTransactionCallback(fragmentTransactionCallback)
    }

    fun unregisterFragmentTransactionCallback() {
        unregisterFragmentTransactionCallback(fragmentTransactionCallback)
    }
}

Here is the link for full sample. You can also put Toolbar to each navHost fragment, it's a little bit simpler.

Which you call in NavHost fragment with Toolbar

val appBarConfig = AppBarConfiguration(navController!!.graph)
dataBinding.toolbar.setupWithNavController(navController!!, appBarConfig)
Ryder answered 28/6, 2020 at 10:30 Comment(5)
can you please send me the sample appEvelynneven
Or can you guide me to work on this solution when my fragments have no toolbarEvelynneven
Github link giving error 404. Can you please update the reference linkEvelynneven
Of course, updated link for this sample. And for entire repo that contains other samplesRyder
@AbdullahJaved, you can also check out this sample which is working app instead of just being a small sample and other than that lets you put dynamic navigation module fragments as base of ViewPager2 and BottomNavigationViewRyder
A
7

A solution for me was to leave the fragment in the ViewPager out of the navigation and directly set the actions on the pages fragment as if these pages were the host. To explain it better :

Say you are in Fragment A with a ViewPager of Fragment B And you try to navigate from B to C

In Fragment B, use ADirections class and an action from A to C. findNavHost().navigateTo(ADirections.ActionFromAtoC)

Allomerism answered 16/4, 2019 at 9:57 Comment(0)
V
3

I have implemented Android Arch Navigations with viewpager. please have a look. Any improvments are welcome. Lets learn togather.

https://github.com/Maqsood007/AndroidJetpack/tree/master/ArchNavViewPagerImpl

Velodrome answered 7/2, 2019 at 8:51 Comment(1)
The only thing that is worked for me, thanks a lot!Overeat
G
1

I have written a related article on this regarding view pagers, specifically focusing on Master-Detail fragments which are tabbed, but the same logic applies to regular ViewPagers. The code is located here.

Gaunt answered 17/4, 2019 at 23:24 Comment(0)
F
1

Thanks to @Marat - he provided great solution. In my case I have list/detail-view navigation for the second ViewPager's view and use Fullscreeen mode without any Action\Tool-bar :

enter image description here

Want to comment some moments:

1) It's possible and easy for me to use one common graph for one page:

<?xml version="1.0" encoding="utf-8"?>
<navigation xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:tools="http://schemas.android.com/tools"
    android:id="@+id/page2Coordinator"
    app:startDestination="@id/Fragment_2Coordinator">

    <fragment
        android:id="@+id/Fragment_2Coordinator"
        android:name="my.app.Fragment_2Coordinator"
        android:label="Fragment_2Coordinator">
        <action
            android:id="@+id/action_showList_2A"
            app:destination="@id/Fragment_2A" />
    </fragment>

    <fragment
        android:id="@+id/Fragment_2A"
        android:name="my.app.Fragment_2A"
        android:label="Fragment_2A">

        <action
            android:id="@+id/action_goToDetail_2B"
            app:destination="@id/Fragment_2B" />
    </fragment>

    <fragment
        android:id="@+id/Fragment_2B"
        android:name="my.app.Fragment_2B"
        android:label="Fragment_2B">

        <action
            android:id="@+id/action_backToList_2A"
            app:destination="@id/Fragment_2A" />
    </fragment>
</navigation>

2) Instead of operations with toolbar in Fragment_2Coordinator.onViewCreated() simply navigate with action in graph (in case you don't use system navigation):

override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
    super.onViewCreated(view, savedInstanceState)
    val navHostFragment = childFragmentManager.findFragmentById(R.id. tab_1_nav_host_fragment) as NavHostFragment? ?: return
    val navController = navHostFragment.navController
    navController.navigate(R.id.action_showList_2A)
}

3) To provide a return from 2B to 2A with phone Back button - go to Activity:

class MainActivity : AppCompatActivity() {

 .  .  .  .  . 

    override fun onBackPressed() {

        val navController = findNavController(R.id.tab_1_nav_host_fragment)

        when(navController.currentDestination?.id) {
            R.id.Fragment_2B -> {
                navController.navigate(R.id.action_backToList_2A)
            }
            else -> {
                super.onBackPressed()
            }
        }
        println()
    }
}
Forrestforrester answered 17/4, 2020 at 18:56 Comment(0)
K
0

I have a MainFragment which is hosting Fragment A, Fragment B and Fragment C inside a viewPager.

And I want to open Fragment D from Fragment B (hosted by viewPager inside MainFragment).

So I created an action from MainFragment to Fragment D and called from Fragment B

val direction = FragmentMainDirections.actionFragmentMainToFragmentD()
findNavController().navigate(direction)

Works.

Kristenkristi answered 23/6, 2019 at 16:27 Comment(1)
Doesn't work when the user clicks a back button the application crash ..!!Exocarp
F
0

In addition to Marat's answer in order to have back stack working with back button in each fragment, you have to add this to your container fragment onViewCreated:

val callback = object : OnBackPressedCallback(true) {
            override fun handleOnBackPressed() {
                if (!navHostFragment.navController.popBackStack()) {
                    isEnabled = false
                    activity?.onBackPressed()
                }
            }
        }
activity?.onBackPressedDispatcher?.addCallback(this, callback)
Faden answered 25/6, 2019 at 5:19 Comment(0)
F
-2

We can implement using bottom navigation component and NavigationGraph easily.

You should create corresponding fragment for every bottom navigation menu

nav_graph.xml

 <?xml version="1.0" encoding="utf-8"?>
    <navigation xmlns:android="http://schemas.android.com/apk/res/android"
        xmlns:app="http://schemas.android.com/apk/res-auto"
        xmlns:tools="http://schemas.android.com/tools"
        android:id="@+id/nav_graph"
        app:startDestination="@id/actionHome">

        <fragment
            android:id="@+id/actionHome"
            android:name="com.sample.demo.fragments.Home"
            android:label="fragment_home"
            tools:layout="@layout/fragment_home">
            <action
                android:id="@+id/toExplore"
                app:destination="@id/actionExplore" />
        </fragment>
        <fragment
            android:id="@+id/actionExplore"
            android:name="com.sample.demo.fragments.Explore"
            android:label="fragment_explore"
            tools:layout="@layout/fragment_explore" />
        <fragment
            android:id="@+id/actionBusiness"
            android:name="com.sample.demo.fragments.Business"
            android:label="fragment_business"
            tools:layout="@layout/fragment_business" />
        <fragment
            android:id="@+id/actionProfile"
            android:name="com.sample.demo.fragments.Profile"
            android:label="fragment_profile"
            tools:layout="@layout/fragment_profile" />

    </navigation>

Every Navigation Fragment ID and bottom navigation menu item id should be same. For Example here

 <fragment
  android:id="@+id/actionBusiness"
 android:name="com.sample.demo.fragments.Business"
                android:label="fragment_business"
                tools:layout="@layout/fragment_business" />

Below bottom Navigation menu navigation.xml

<?xml version="1.0" encoding="utf-8"?>
<menu xmlns:android="http://schemas.android.com/apk/res/android">

    <item
        android:id="@+id/actionExplore"
        android:icon="@drawable/ic_search_24dp"
        android:title="@string/explore" />

    <item
        android:id="@+id/actionBusiness"
        android:icon="@drawable/ic_business_24dp"
        android:title="@string/business" />

    <item
        android:id="@+id/actionProfile"
        android:icon="@drawable/ic_profile_24dp"
        android:title="@string/profile" />


</menu>

Set the nav_graph.xml to palceholder fragment in activity_main.xml

<?xml version="1.0" encoding="utf-8"?>
<android.support.constraint.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:tools="http://schemas.android.com/tools"
    android:id="@+id/container"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:background="@drawable/gradient_bg"
    android:focusable="true"
    android:focusableInTouchMode="true"
    tools:context=".MainActivity"
    tools:layout_editor_absoluteY="25dp">

    <android.support.design.widget.BottomNavigationView
        android:id="@+id/navigation"
        android:layout_width="0dp"
        android:layout_height="wrap_content"
        android:layout_marginStart="0dp"
        android:layout_marginEnd="0dp"
        android:background="@color/semi_grey"
        app:itemIconTint="@drawable/bottom_bar_nav_item"
        app:itemTextColor="@drawable/bottom_bar_nav_item"
        app:labelVisibilityMode="labeled"
        app:layout_constraintBottom_toBottomOf="parent"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintLeft_toLeftOf="parent"
        app:layout_constraintRight_toRightOf="parent"
        app:layout_constraintStart_toStartOf="parent"
        app:menu="@menu/navigation" />

    <include
        android:id="@+id/appBarLayout"
        layout="@layout/app_bar"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toTopOf="parent" />


    <fragment
        android:id="@+id/mainNavigationFragment"
        android:name="androidx.navigation.fragment.NavHostFragment"
        android:layout_width="0dp"
        android:layout_height="0dp"
        android:paddingBottom="@dimen/activity_horizontal_margin"
        app:defaultNavHost="true"
        app:layout_constraintBottom_toTopOf="@+id/navigation"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toBottomOf="@+id/appBarLayout"
        app:navGraph="@navigation/nav_graph" />

</android.support.constraint.ConstraintLayout>

Mapping the Navigation Graph into fragment here app:navGraph="@navigation/nav_graph"

After that implement navigation graph and bottomNavigation component in MainActivity.java

 BottomNavigationView navigation = (BottomNavigationView) findViewById(R.id.navigation);
        NavController navController = Navigation.findNavController(this, R.id.mainNavigationFragment);
        NavigationUI.setupWithNavController(navigation, navController); 

Cheers!!!

Frank answered 2/1, 2019 at 5:16 Comment(2)
The problem is regarding ViewPager implementation with arch navigation.Velodrome
I have the same problem, i can't implement navigation architecture to the into a view pager using a tabsLeonialeonid

© 2022 - 2024 — McMap. All rights reserved.