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
What worked for me so far:
In navigation_graph.xml
in nested graph:
I didnt need to change the ViewPager, and Directions were created for the Child Fragments so navigation is possible from there.
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)
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
/*
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
andTabLayout
- Referring to Youtube training.
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.