New navigation component from arch with nested navigation graph

后端 未结 7 1072
栀梦
栀梦 2020-12-07 11:26

I have one case and wish to implement it by arch navigation component. For example I have 2 Nav Graphs (main and nested). Can I call main graph from nested and how?

相关标签:
7条回答
  • 2020-12-07 11:42

    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 NavHosts. 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)
    }
    
    0 讨论(0)
  • 2020-12-07 11:42

    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 {
    
                //                                                                     
    0 讨论(0)
  • 2020-12-07 11:54

    Same problem, all examples found on the web iterate over the basic setup. I have an nav graph in main activity which starts with login fragment, after login goes to main fragment witch has another nav graph with bottom navigation view. The problem is that the main fragment is tied to main activity nav graph. I think only the fragments inside the main fragment can use the main fragment nav graph.

    0 讨论(0)
  • 2020-12-07 11:57

    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)
    
    0 讨论(0)
  • 2020-12-07 11:59

    Actually is working, using

    val host: NavHostFragment? = (childFragmentManager.findFragmentById(R.id.main_app_fragment_container)  as NavHostFragment?)
    

    I can navigate from main fragment

    0 讨论(0)
  • 2020-12-07 11:59

    I had the same issue, the error didn't disappear but the problem with the virtual editor not displaying got solved, so it might help someone else too;

    If you work with Kotlin, check to see whether your dependencies have the same Kotlin versions working, then rebuild/sync, takes a bit to load, and at least for me seems to be working fine.

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