FragmentScenario 和嵌套的 NavHostFragments 未在 Instrumentation 测试中按预期执行导航

FragmentScenario and nested NavHostFragments don't perform navigations as expected in Instrumentation tests

我正在编写一个 Activity 应用程序,它使用 Android 的导航组件来帮助进行导航和片段场景以进行仪器测试。在实际应用程序导航行为与使用片段场景的 Instrumentation 测试期间隔离测试的片段行为之间使用后退按钮时,我 运行 遇到性能差异。

在我的 MainActivity 中,我有一个占据整个屏幕的主要 NavHostFragment。我使用那个导航主机片段来显示几个屏幕,包括一些主细节片段。每个主要细节片段中都有另一个 NavHostFragment 以显示该功能的不同细节片段。此设置效果很好,提供了我想要的行为。

为了完成主细节屏幕,我使用具有两个 FrameLayout 的 ParentFragment 为平板电脑和手机创建分屏,我以编程方式隐藏其中一个 FrameLayout。创建 ParentFragment 时,它会检测它是否在平板电脑或手机上 运行,然后以编程方式将 NavHostFragment 添加到平板电脑上的右框架布局,并在手机上隐藏右窗格,将 NavHostFragment 添加到左窗格. NavHostFragments 还设置了不同的导航图,具体取决于它们是 运行 在平板电脑还是手机上(在手机上我们将片段显示为对话框,在平板电脑上我们将它们显示为常规片段)。

 private fun setupTabletView() {
        viewDataBinding.framelayoutLeftPane.visibility = View.VISIBLE
        if (navHostFragment == null) {
            navHostFragment = NavHostFragment.create(R.navigation.transport_destinations_tablet)
            navHostFragment?.let {
                childFragmentManager.beginTransaction()
                    .add(R.id.framelayout_left_pane, it, TRANSPORT_NAV_HOST_TAG)
                    .setPrimaryNavigationFragment(it)
                    .commit()
            }
        }

        if (childFragmentManager.findFragmentByTag(SummaryFragment.TAG) == null) {
            childFragmentManager.beginTransaction()
                .add(R.id.framelayout_right_pane, fragFactory.instantiate(ClassLoader.getSystemClassLoader(), SummaryFragment::class.java.canonicalName!!), SummaryFragment.TAG)
                .commit()
        }
    }

    private fun setupPhoneView() {
        viewDataBinding.framelayoutLeftPane.visibility = View.GONE
        if (navHostFragment == null) {
            navHostFragment = NavHostFragment.create(R.navigation.transport_destinations_phone)
            navHostFragment?.let {
                childFragmentManager.beginTransaction()
                        .replace(R.id.framelayout_left_pane, it, TRANSPORT_NAV_HOST_TAG)
                    .setPrimaryNavigationFragment(it)
                    .commit()
            }
        }
    }

当 运行使用应用程序的 devDebug 版本时,一切都按预期工作。我能够使用主 NavHostFragment 导航到不同的主从屏幕。在我导航到主细节屏幕后,嵌套的 NavHostFragment 接管,我可以使用嵌套的 NavHostFragment 导航屏幕进出主细节片段。

当用户尝试单击后退按钮时,这会导致离开主详细信息屏幕并导航到上一个屏幕,我们会向用户弹出一个对话框,询问他们是否真的想离开屏幕(这是他们输入大量数据的屏幕)。为此,我们注册了一个 onBackPressDispatcher 回调,以便我们知道何时按下后退按钮并在调用回调时导航到对话框。在 devDebug 版本中,用户首先位于导航图上的位置 A。如果他们在位置 A 时单击后退按钮,那么我们会显示一个对话框片段,询问用户是否真的打算离开屏幕。相反,如果用户从位置 A 导航到位置 B 并单击返回,他们将首先导航回位置 A。如果他们再次单击后退按钮,将调用后退按钮调度程序回调,然后向他们显示对话框片段,询问是否他们真的打算离开位置 A。所以看起来后退按钮会影响嵌套 NavHostFragment 的返回堆栈,直到嵌套 NavHostFragment 只剩下一个片段。当只剩下一个片段并单击后退按钮时,将调用 onBackPressDisapatcher 回调。这正是所需的行为。但是,当我使用 Fragment Scenario 编写 Instrumentation 测试时,我尝试测试 ParentFragment,我发现后按行为不同。在测试中,我使用 Fragment Scenario 来启动 ParentFragment,然后 运行 一个测试,我在嵌套的 NavHostFragment 中进行导航。当我单击后退按钮时,我希望嵌套的导航主机片段会弹出其堆栈。但是,会立即调用 onBackPressDispatcher 回调,而不是在嵌套的导航主机片段在其堆栈上留下一个片段之后调用。

我在 NavHostFragment 中设置了一些断点,当测试 运行 时,NavHostFragment 似乎没有设置为拦截返回点击。它的 enableOnBackPressed() 方法总是在调用时将标志设置为 false。

我不明白是什么测试设置导致了这种行为。我认为导航主机片段会自行拦截后退点击,直到它的后台堆栈上只剩下一个片段,然后才会调用 onBackPressDispatcher 回调。

我是不是误解了我应该如何测试它?为什么在按下后退按钮时会调用 onBackPressDispatcher 的回调。

正如在 FragmentScenario source code 中看到的那样,它目前 (自 Fragment 1.2.1 起)使用 setPrimaryNavigationFragment()。这意味着被测试的 Fragment 不会拦截后退按钮,因此,它的子片段(例如您的 NavHostFragment)不会拦截后退按钮。

您可以在测试中自己设置此标志:

@Test
fun testParentFragment() {
    // Use the reified Kotlin extension to launchFragmentInContainer
    with(launchFragmentInContainer<ParentFragment>()) {
        onFragment { fragment ->
            // Use the fragment-ktx commitNow Kotlin extension
            fragment.parentFragmentManager.commitNow {
                setPrimaryNavigationFragment(fragment)
            }
        }
        // Now you can proceed with your test
    }