New navigation component from arch with nested navigation graph
The point is to get the right NavController
to navigate in the right graph.Let's take this scenario as an example:
MainActivity|- MainNavHost |- NavBarFragment | |- NestedNavHost | | |-NestedContentFragment1 | | |-NestedContentFragment2 | | | |- BottomNavigationView | |- LoginFragment
The main graph and the nested graph are in separate xml files: this is required, as far as I understood, because the navigations target different layout areas, so they require two different NavHost
s. Each Navhost
will need to reference its graph by id, which requires them to be in different resource files.
The point is that to navigate in a specific graph, we must get a reference to the right graph's owner: to do this, when calling Navigation.findNavController(view)
, the view
argument is crucial.
Docs say that
NavHostFragments register their navigation controller at the root of their view subtree such that any descendant can obtain the controller instance through the Navigation helper class's methods
So for example, if inside NavBarFragment
we write
override fun onViewCreated(view: View, savedInstanceState: Bundle?) { navController = Navigation.findNavController(view)}
here view
is a parent of the NestedNavHost
(that is the nested NavHostFragment
), not a descendant, meaning that findNavController
will search upstream in the tree and will return the MainNavHost
's NavController
.
If instead we write
override fun onViewCreated(view: View, savedInstanceState: Bundle?) { val nestedNavHostFragment = childFragmentManager.findFragmentById(R.id.nestedNavHostFragment) as? NavHostFragment navController = nestedNavHostFragment?.navController}
where nestedNavHostFragment
is the id of the FragmentContainerView
in the layout, we get a reference to the correct NestedNavHost
. Note the use of childFragmentManager
, not parentFragmentManager
.
In case you're still using the deprecated xml <fragment>
tag, you can write
override fun onViewCreated(view: View, savedInstanceState: Bundle?) { val fragmentContainer = view.findViewById<View>(R.id.nestedNavHostFragment) navController = Navigation.findNavController(fragmentContainer)}
where nestedNavHostFragment
is the id of the <fragment>
tag. We get a reference to the correct NestedNavHost
now, because the view we pass to findNavController
belongs to the NestedNavHost
's subtree.
Similarly, if you need to get a reference to the main NavController
from inside a NestedContentFragment
, here's what we can do:
override fun onViewCreated(view: View, savedInstanceState: Bundle?) { // we can get the innermost NavController using this view, // because we are inside its subtree: nestedNavController = Navigation.findNavController(view) // we can find the outer NavController passing the owning Activity // and the id of a view associated to that NavController, // for example the NavHostFragment id: mainNavController = Navigation.findNavController(activity!!, R.id.mainNavHostFragment)}
Actually you could use Global actions to navigate from a nested nav graph destination to a main nav graph destination.
Create a global action from nested nav graph to desired destination in main nav graph (highlighted in the image below)
example:
<navigation android:id="@+id/main_nav_graph" ... > <fragment android:id="@+id/fragStart" .../> <fragment .../> <fragment .../> <navigation android:id="@+id/nested_nav_graph"> ... <!-- Global Action --> <action android:id="@+id/action_global_start" app:destination="@id/fragStart" /> </navigation></navigation>
To navigate to main graph destination use
findNavController().navigate(R.id.action_global_start)
I created an answer with the info devrocca provided. It's a full answer from scratch, i didn't skip anything if anyone ever needs.
This is the main fragment for navigation. Camera is direct destination without any nested graph, Dashboard has it's own nested graph but it's added to same backstack camera fragment is added. Home has 3 fragments with it's own nav host
MainActivity|- MainNavHost |- HomeNavHostFragment | |- NestedNavHost | |-HomeFragment1 | |-HomeFragment2 | |-HomeFragment3 | |- nav_graph_dashboard | |- CameraFragment
Here is the navigation files
Main Navigation 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/main_dest"> <!-- MainFragment--> <fragment android:id="@+id/main_dest" android:name="com.smarttoolfactory.tutorial1_3navigation_nestednavhost.blankfragment.MainFragment" android:label="MainFragment" tools:layout="@layout/fragment_main"> <!-- Camera --> <action android:id="@+id/action_main_dest_to_cameraFragment" app:destination="@id/cameraFragment" /> <!-- Home NavGraph --> <action android:id="@+id/action_main_dest_to_nav_graph_home" app:destination="@id/nav_graph_home" /> <!-- Dashboard NavGraph--> <action android:id="@+id/action_main_dest_to_nav_graph_dashboard" app:destination="@id/nav_graph_dashboard" /> </fragment> <!-- Camera --> <fragment android:id="@+id/cameraFragment" android:name="com.smarttoolfactory.tutorial1_3navigation_nestednavhost.blankfragment.CameraFragment" android:label="CameraFragment" /> <!-- Home--> <include app:graph="@navigation/nav_graph_home" /> <!-- Dashboard--> <include app:graph="@navigation/nav_graph_dashboard" /> <!-- Global Action Start --> <action android:id="@+id/action_global_start" app:destination="@id/main_dest" app:popUpTo="@id/main_dest" app:popUpToInclusive="true" /></navigation>
Dashboard nested navigation graph
<?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/dashboard_dest"> <fragment android:id="@+id/dashboard_dest" android:name="com.smarttoolfactory.tutorial1_3navigation_nestednavhost.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.tutorial1_3navigation_nestednavhost.blankfragment.DashboardFragment2" android:label="DashboardFragment2" tools:layout="@layout/fragment_dashboard2"> </fragment></navigation>
And nested navigation graph with it's own NavHost nav_graph_home
<?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_home" app:startDestination="@id/home_dest"> <fragment android:id="@+id/home_dest" android:name="com.smarttoolfactory.tutorial1_3navigation_nestednavhost.blankfragment.HomeNavHostFragment" android:label="HomeHost" tools:layout="@layout/fragment_home_navhost" /> <fragment android:id="@+id/homeFragment1" android:name="com.smarttoolfactory.tutorial1_3navigation_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.tutorial1_3navigation_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.tutorial1_3navigation_nestednavhost.blankfragment.HomeFragment3" android:label="HomeFragment3" tools:layout="@layout/fragment_home3" /></navigation>
Layouts, i only add necessary ones, others are simple layouts with buttons, i add link for sample project with other navigation components samples included.
MainActivity<?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.fragment.app.FragmentContainerView android:id="@+id/main_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>
Main Fragment, this is first fragment that shown in the image used as start of main navigation
<?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:id="@+id/parentLayout" android:layout_width="match_parent" android:layout_height="match_parent"> <Button android:id="@+id/btnDestCam" android:layout_width="wrap_content" android:layout_height="wrap_content" android:text="Destination Camera" app:layout_constraintBottom_toTopOf="@+id/btnNavGraphHome" app:layout_constraintHorizontal_bias="0.5" app:layout_constraintLeft_toRightOf="parent" app:layout_constraintRight_toLeftOf="parent" app:layout_constraintTop_toTopOf="parent" /> <Button android:id="@+id/btnNavGraphHome" android:layout_width="wrap_content" android:layout_height="wrap_content" android:text="Nested NavHost Graph Home" app:layout_constraintBottom_toTopOf="@+id/btnNavGraphDashboard" app:layout_constraintHorizontal_bias="0.5" app:layout_constraintLeft_toRightOf="parent" app:layout_constraintRight_toLeftOf="parent" app:layout_constraintTop_toBottomOf="@+id/btnDestCam" /> <Button android:id="@+id/btnNavGraphDashboard" android:layout_width="wrap_content" android:layout_height="wrap_content" android:text="Nested Graph Dashboard" app:layout_constraintBottom_toBottomOf="parent" app:layout_constraintHorizontal_bias="0.5" app:layout_constraintLeft_toRightOf="parent" app:layout_constraintRight_toLeftOf="parent" app:layout_constraintTop_toBottomOf="@+id/btnNavGraphHome" /> </androidx.constraintlayout.widget.ConstraintLayout></layout>
Layout that contains inner NavHostFragment
for home navigation
<?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" 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>
MainActivity is for checking main navigation back stack, important thing here is
supportFragmentManager back stack is not updated as you navigate it's childFragmentManager even for main navigation, even if you only have one
class MainActivity : AppCompatActivity() { override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) setContentView(R.layout.activity_main) // 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 } }}
Fragment that contains Home navigation's host
class HomeNavHostFragment : BaseDataBindingFragment<FragmentHomeNavhostBinding>() { override fun getLayoutRes(): Int = R.layout.fragment_home_navhost private var navController: NavController? = null override fun onViewCreated(view: View, savedInstanceState: Bundle?) { super.onViewCreated(view, savedInstanceState) val nestedNavHostFragment = childFragmentManager.findFragmentById(R.id.nested_nav_host_fragment) as? NavHostFragment navController = nestedNavHostFragment?.navController navController?.navigate(R.id.homeFragment1) listenBackStack() } private fun listenBackStack() { // Get NavHostFragment val navHostFragment = childFragmentManager.findFragmentById(R.id.nested_nav_host_fragment) // ChildFragmentManager of the current NavHostFragment val navHostChildFragmentManager = navHostFragment?.childFragmentManager navHostChildFragmentManager?.addOnBackStackChangedListener { val backStackEntryCount = navHostChildFragmentManager!!.backStackEntryCount val fragments = navHostChildFragmentManager!!.fragments Toast.makeText( requireContext(), "HomeNavHost backStackEntryCount: $backStackEntryCount, fragments: $fragments", Toast.LENGTH_SHORT ).show() } val callback = object : OnBackPressedCallback(true) { override fun handleOnBackPressed() { val backStackEntryCount = navHostChildFragmentManager!!.backStackEntryCount Toast.makeText( requireContext(), "HomeNavHost backStackEntryCount: $backStackEntryCount", Toast.LENGTH_SHORT ).show() if (backStackEntryCount == 1) { OnBackPressedCallback@ this.isEnabled = false requireActivity().onBackPressed() } else { navController?.navigateUp() } } } requireActivity().onBackPressedDispatcher.addCallback(viewLifecycleOwner, callback) }}
There is one thing i don't know if it's improved in graph or code with nested NavHostFragment
If you set start destination of nav_graph_home HomeFragment1
instead of HomeNavHostFragment
it works as dashboard which ignores nested NavHost and added to main back stack of fragments.
Since you are in inner NavHostFragment findNavController() in any home fragment returns the inner one
class HomeFragment3 : BaseDataBindingFragment<FragmentHome3Binding>() { override fun getLayoutRes(): Int = R.layout.fragment_home3 private var count = 0 override fun onViewCreated(view: View, savedInstanceState: Bundle?) { super.onViewCreated(view, savedInstanceState) dataBinding.btnIncrease.setOnClickListener { dataBinding.tvTitle.text = "Count: ${count++}" } val mainNavController = Navigation.findNavController(requireActivity(), R.id.main_nav_host_fragment) dataBinding.btnGoToStart.setOnClickListener { // đŸ”¥Using destination belong to main_nav_host with nested navHost causes app to crash// findNavController().navigate(R.id.action_global_start) mainNavController.navigate(R.id.action_global_start)/**/ } }}
You can also use global action but it's not required since back navigation in inner navHost directly moves you back to main navigation if you don't use OnBackPressed
.
Link for full example and the other nav component samples if you are interested.