【问题标题】:New navigation component from arch with nested navigation graph带有嵌套导航图的 Arch 新导航组件
【发布时间】:2018-11-16 17:38:47
【问题描述】:

我有一个案例,希望通过拱形导航组件来实现。例如,我有 2 个导航图(主图和嵌套图)。我可以从嵌套调用主图吗?如何调用?

【问题讨论】:

  • @IanLake 我们也可以为导航图使用回调吗?还是finishWithResult 模拟?新的 android 导航对于简单的屏幕来说太强大了,但对于嵌套的 Fragment 就没有那么有用了。或者我们应该为嵌套片段创建活动......
  • 我也有同样的问题你找到解决办法了吗??

标签: android android-architecture-components android-navigation android-jetpack


【解决方案1】:

关键是让正确的NavController 在正确的图表中导航。 我们以这个场景为例:

MainActivity
|- MainNavHost
   |- NavBarFragment
   |  |- NestedNavHost
   |  |  |-NestedContentFragment1
   |  |  |-NestedContentFragment2
   |  |
   |  |- BottomNavigationView
   |
   |- LoginFragment

主图和嵌套图在单独的 xml 文件中:据我了解,这是必需的,因为导航针对不同的布局区域,因此它们需要两个不同的NavHosts。每个Navhost 都需要通过 id 引用它的图,这要求它们位于不同的资源文件中。

关键是要在特定图表中导航,我们必须获得对正确图表所有者的引用:为此,在调用 Navigation.findNavController(view) 时,view 参数至关重要。

文档这么说

NavHostFragments 在其视图子树的根目录中注册其导航控制器,以便任何后代都可以通过导航辅助类的方法获取控制器实例

例如,如果在NavBarFragment 内部我们写

override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
    navController = Navigation.findNavController(view)
}

这里viewNestedNavHost(即嵌套的NavHostFragment)的父代,而不是后代,这意味着findNavController 将在树中搜索上游并返回MainNavHostNavController

如果我们改为写

override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
    val nestedNavHostFragment = childFragmentManager.findFragmentById(R.id.nestedNavHostFragment) as? NavHostFragment
    navController = nestedNavHostFragment?.navController
}

其中nestedNavHostFragment 是布局中FragmentContainerView 的id,我们得到了对正确NestedNavHost 的引用。注意使用childFragmentManager,而不是parentFragmentManager

如果您仍在使用已弃用的 xml <fragment> 标签,您可以编写

override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
    val fragmentContainer = view.findViewById<View>(R.id.nestedNavHostFragment)
    navController = Navigation.findNavController(fragmentContainer)
}

其中nestedNavHostFragment&lt;fragment&gt; 标签的ID。我们现在得到了对正确NestedNavHost 的引用,因为我们传递给findNavController 的视图属于NestedNavHost 的子树。

同样,如果您需要从 NestedContentFragment 内部获取对主要 NavController 的引用,我们可以这样做:

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

【讨论】:

  • 终于弄明白了...这很棘手,而且在其他任何地方都没有文档,甚至在 Google 的开发者网站上也没有。 Google 给出的“嵌套导航”示例在同一个 xml 中有嵌套导航,您无法在 BottomNavFragment 中引用它。您将必须有两个 xml 图表。而且我发现它不是很贴切,一些导航来自嵌套片段内部,但是要“以模态方式呈现”,因此您必须从父图“导航”它。
  • fragment tag in the layout 是什么意思?
  • 我的意思是xml布局中&lt;fragment /&gt;id。截至今天,它已被弃用,取而代之的是 FragmentContainerView。见here
  • 我们如何在 NestedNavHost 中的片段中添加工具栏后退按钮?提前致谢!
  • 编辑以考虑FragmentContainerView 的新策略。
【解决方案2】:

实际上,您可以使用Global actions 从嵌套导航图目标导航到主导航图目标。

创建一个从嵌套导航图到主导航图中所需目的地的全局操作(在下图中突出显示)

示例:

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

要导航到主图目的地,请使用

findNavController().navigate(R.id.action_global_start)

【讨论】:

  • 刚刚回到这个问题,我想强调这不是原始问题的解决方案。图表的嵌套是在视图级别,而不是在导航级别,因此您需要 2 个 NavHost 来实现 OP 结构。您不能将一个图表嵌套在另一个图表中,您需要 2 个单独的图表,每个图表彼此都不知道,因此您无法将它们与全局操作链接。
【解决方案3】:

我使用提供的信息 devrocca 创建了一个答案。这是从头开始的完整答案,如果有人需要,我没有跳过任何内容。

这是导航的主要片段。相机是没有任何嵌套图的直接目的地,仪表板有它自己的嵌套图,但它被添加到相同的 backstack 相机片段被添加。 Home 有 3 个片段和它自己的导航主机

MainActivity
|- MainNavHost
   |- HomeNavHostFragment
   |  |- NestedNavHost
   |     |-HomeFragment1
   |     |-HomeFragment2
   |     |-HomeFragment3
   |  
   |- nav_graph_dashboard 
   |
   |- CameraFragment

这里是导航文件

主导航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>

仪表板嵌套导航图

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

嵌套导航图和它自己的 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>

布局,我只添加必要的,其他是带有按钮的简单布局,我添加了示例项目的链接,其中包括其他导航组件示例。

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>

主片段,这是图像中显示的第一个片段,用作主导航的开始

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

包含用于主页导航的内部 NavHostFragment 的布局

<?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 用于检查主导航返回堆栈,这里重要的是

supportFragmentManager 后栈在您导航时不会更新,它是 childFragmentManager,即使是主导航,即使您只有一个

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

包含主页导航主机的片段

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)

    }
}

有一件事我不知道它是否在使用嵌套 NavHostFragment 的图形或代码中得到了改进

如果您将 nav_graph_home 的起始目的地设置为 HomeFragment1 而不是 HomeNavHostFragment,它将作为仪表板工作,忽略嵌套的 NavHost 并添加到主要的后台片段堆栈。

由于您在内部 NavHostFragment 中,因此任何主片段中的 findNavController() 都会返回内部片段

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

您也可以使用全局操作,但这不是必需的,因为如果您不使用 OnBackPressed,内部 navHost 中的后退导航会直接将您返回到主导航。

full example 和其他导航组件示例的链接(如果您有兴趣)。

【讨论】:

  • 你对此有什么想法吗?我已经做了一个演示,但是如果有任何想法请帮助我github.com/sunil-singh-chaudhary/Jet-Navigation-Fragments
  • 如何从您链接的仓库构建示例?克隆 repo 时,只能将“External-Tutorial-Navigation-Codelab”模块构建为应用程序。
  • @akubi 你用的是哪个 Android Studio 版本?我将 repo 下载为 zip,我使用的是 Android Studio 4.2 Canary 14,要求我使用 此项目使用版本 4.1.0beta04 gradle 插件 更新 gradle,我选择了开始更新,它对我来说效果很好。我可以看到每个模块并尝试运行一些模块,它们运行良好
【解决方案4】:

实际上正在工作, 使用

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

我可以从主要片段导航

【讨论】:

  • 好的,您可以从主导航图导航子导航图,但我需要从子导航图导航主图。导航图的大问题是我无法从孩子那里得到回调
  • 您可以通过Navigation.findNavController(requireActivity,R.id.parent_nav_host_fragment).navigate(R.id.action) 从子图导航主图。您还可以使用 LiveData 通过共享 ViewModel 从子级与父级进行通信。
【解决方案5】:

我找到了解决内部 NavController 被覆盖问题的临时解决方案。 您可以使用自定义 NavHostFragment,它为您提供所需的 navController。 我的代码:

<androidx.fragment.app.FragmentContainerView
        ...
        android:name="MyNavHostFragment"
        app:defaultNavHost="false"
        app:navGraph="@navigation/inner_nav">
        ...
    </androidx.fragment.app.FragmentContainerView>

...

class MyNavHostFragment: NavHostFragment() {

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        MainFragment.innerNavController = navController
    }
}

...

class MainFragment : Fragment() {
    companion object{
        lateinit var innerNavController: NavController
    }

    override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
        super.onViewCreated(view, savedInstanceState)
        val bottomNavigationView = 
             view!!.findViewById<BottomNavigationView>(R.id.bottom_navigation_view)
        bottomNavigationView.setupWithNavController(innerNavController)
    }
}

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 2017-06-11
    • 1970-01-01
    • 1970-01-01
    • 2019-07-28
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多