Android 导航组件:回击导致 NavController 不同步

Android Navigation Component: Hitting back results in NavController being out of sync

我正在通过 FragmentManager 将应用程序从手动后台操作迁移到使用新的导航库。该应用程序包含一个 MainActivity,一个 DrawerLayout,抽屉中有 10-12 个项目。当点击时,会发生一些片段操作并显示一个新片段。

此生产应用程序中的代码太多,简单地删除旧的导航结构并整体移动到导航库是不可行的。

相反,我正在做的是逐个抽屉项目迁移抽屉项目。例如 - 有一个称为支票存款的功能,可通过导航抽屉中的项目访问。当你点击那个项目时,你会看到一个新的片段,你可以通过一个类似于支票存款向导的流程。到目前为止,我所做的是创建一个新的 RootCheckDeposit 片段,其 View 由一个包含导航图的 FragmentContainerView 组成。然后在最终包含在 RootcheckDeposit.

中的导航图中导航该支票存款向导的整个“流程”

这是 RootCheckDeposit 片段的布局:

<androidx.fragment.app.FragmentContainerView xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    android:id="@+id/root"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:name="androidx.navigation.fragment.NavHostFragment"
    app:defaultNavHost="true"
    app:navGraph="@navigation/check_deposit_graph" />

虽然与工具栏的集成仍然有点不稳定,但大部分工作正常。

但我遇到的主要问题是,当您逐步完成此向导时,回击可能会导致 NavController 进入一种状态,它似乎认为它位于与实际不同的片段上是。这确实发生在支票存款向导的以下流程中:

加载屏幕 -> 条款屏幕 -> 支票存款登陆屏幕。

进入支票存款登录屏幕后,如果您回击,则什么也不会发生 - 这很好。不好的是,如果您随后单击该登录屏幕中的一个按钮,该按钮将引导您完成向导,应用程序会崩溃并显示 IllegalArgumentException,声称我们正在尝试使用 [=23] 中不可用的操作=].这很奇怪,因为我们不在 Loading Screen,我们在 Check Deposit Landing Screen

这是我的导航图文件以供参考:

<?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/check_deposit_graph"
    app:startDestination="@id/loadingFragment">

    <fragment
        android:id="@+id/loadingFragment"
        android:name="module.checkdeposit.loading.TermsLoadingFragment"
        android:label="@string/check_deposit_title"
        tools:layout="@layout/terms_loading_fragment" >
        <action
            android:id="@+id/action_loadingFragment_to_termsAndConditionsFragment"
            app:destination="@id/termsAndConditionsFragment"
            app:popUpTo="@id/loadingFragment"
            app:popUpToInclusive="true" />
        <action
            android:id="@+id/action_loadingFragment_to_depositOptionsFragment"
            app:destination="@id/depositLandingFragment"
            app:popUpTo="@id/loadingFragment"
            app:popUpToInclusive="true" />
    </fragment>

    <fragment
        android:id="@+id/termsAndConditionsFragment"
        android:name="module.checkdeposit.terms.TermsAndConditionsFragment"
        android:label="@string/check_deposit_terms_and_conditions"
        tools:layout="@layout/terms_and_conditions_fragment" >
        <action
            android:id="@+id/action_termsAndConditionsFragment_to_depositLandingFragment"
            app:destination="@id/depositLandingFragment"
            app:popUpTo="@id/termsAndConditionsFragment"
            app:popUpToInclusive="true" />
    </fragment>

    <fragment
        android:id="@+id/depositLandingFragment"
        android:name="module.checkdeposit.depositlanding.DepositLandingFragment"
        android:label="@string/check_deposit_title"
        tools:layout="@layout/deposit_landing_fragment" >
        <action
            android:id="@+id/action_depositLandingFragment_to_depositHistoryFragment"
            app:destination="@id/depositHistoryFragment" />
        <action
            android:id="@+id/action_depositLandingFragment_to_scanCheckFragment"
            app:destination="@id/scanCheckFragment" />
    </fragment>

    <fragment
        android:id="@+id/depositHistoryFragment"
        android:name="module.checkdeposit.deposithistory.DepositHistoryFragment"
        android:label="@string/check_deposit_deposit_history"
        tools:layout="@layout/deposit_history_fragment" />

    <fragment
        android:id="@+id/scanCheckFragment"
        android:name="module.checkdeposit.scancheck.ScanCheckFragment"
        android:label=""
        tools:layout="@layout/scan_check_fragment" />
</navigation>

值得注意 - 当我们从 Loading screenTerms screen 再到 Landing Screen 时,我们弹出每个项目,因为我不希望用户能够“返回” " 到加载屏幕或条款屏幕。

我尝试过的事情:

我还梳理了实际的 NavController 代码,似乎正在发生的事情是 NavController 在单击后退时弹出其内部后退堆栈但不更改目的地,因为确实有没有其他目的地可去。看起来 NavController 处于一种奇怪的不一致状态,它和当前显示的 Fragment 不在 whack/not 指向同一事物。

有没有其他人成功地完成了这种迁移?

编辑:我创建了一个 repo,其中有一个最小的项目来重现我 运行 遇到的崩溃:https://github.com/alexsullivan114/ExampleNavigationCrashRepo。这种回购模仿了我正在处理的生产应用程序的结构。如果您单击幻灯片抽屉项目,然后继续单击屏幕中的文本,直到到达 Next Example Fragment,然后返回几次并再次单击该文本,您将看到崩溃。

我确定问题在于我如何设置导航库,我只是不确定正确的方法是什么不重写其余的应用程序导航

根据 considerations for child and sibling fragments guide:

Only one FragmentManager is allowed to control the fragment back stack at any given time. If your app shows multiple sibling fragments on the screen at the same time, or if your app uses child fragments, then one FragmentManager must be designated to handle your app's primary navigation.

To define the primary navigation fragment inside of a fragment transaction, call the setPrimaryNavigationFragment() method on the transaction, passing in the instance of the fragment whose childFragmentManager should have primary control.

Interact programmatically with Navigation 中所述,app:defaultNavHost="true"NavHostFragment 本身为您调用 setPrimaryNavigationFragment(),但您 不是 在父片段上调用 ​​setPrimaryNavigationFragment() - 你的 RootSlideshowFragment。这意味着正在调用 none 的逻辑来正确地自动处理后退按钮 - 你基本上打破了从 activity 到父片段的链到 NavHostFragment。这会导致您拦截后退按钮,即使没有后退堆栈(当您弹出最后一个片段时,导航实际上不会删除它,否则当您的 activity 的退出动画是 [=43 时它不会出现=]).

因此要解决此问题,您需要:

  1. RootSlideshowFragment 中删除你的 OnBackPressedCallback。不需要。
  2. 当您在 MainActivity 中的片段之间交换时,在新片段上调用 ​​setPrimaryNavigationFragment()
navView.setNavigationItemSelectedListener { menuitem ->
    val newFragment = when (menuitem.itemId) {
        R.id.nav_home -> HomeFragment()
        R.id.nav_gallery-> GalleryFragment()
        else -> RootSlideshowFragment()
    }
    supportFragmentManager.beginTransaction()
        .replace(R.id.container, newFragment)
        .setPrimaryNavigationFragment(newFragment)
        .commit()
    true
}