I'm in the process of migrating an app from manual backstack manipulation via FragmentManager
to using the new navigation library. The app consists of a MainActivity
that has a DrawerLayout
with 10-12 items in the drawer. When tapped some fragment manipulation happens and a new fragment is shown.
There's enough code in this production application that it's not feasible to simply rip out the old navigation structure and move to the navigation library whole cloth.
Instead, what I'm doing is migrating drawer item by drawer item. For example - There's a feature called check deposit which is accessed via an item in the nav drawer. When you tap that item, you're shown a new fragment and you can progress through a check deposit wizard-like flow. So far what I've done is created a new RootCheckDeposit
fragment whose View
consists of a single FragmentContainerView
that houses a nav graph. Then the whole "flow" of that check deposit wizard is navigated through in the nav graph that's ultimately contained in the RootcheckDeposit
.
Here's the layout for that RootCheckDeposit
fragment:
<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" />
That's working mostly ok, though the integration with the toolbar is still a bit wonky.
But the main problem I'm having is that as you progress through this wizard, hitting back can cause the NavController
to get into a state where it seems to think it's on a different fragment than it actually is. This is reliably happening in the following flow within the check deposit wizard:
Loading screen -> Terms screen -> Check deposit landing screen.
Once you get to the check deposit landing screen, if you hit back nothing happens - which is fine. What's not fine is if you then click on a button in that landing screen that would progress you through the wizard the app crashes with an IllegalArgumentException
, claiming that we're trying to use an action that's not available from Loading Screen
. Which is weird because we're not on the Loading Screen
, we're on the Check Deposit Landing Screen
.
Here's my nav graph file for reference:
<?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>
Of note - when we go from Loading screen
to Terms screen
to Landing Screen
we're popping each item because I don't want the user to be able to go "back" to a loading screen or a terms screen.
Things I've tried:
popUpTo
logic in the nav graph to point to the actual nav graph instead of the destinationnavController.popBackStack
is true
or false
and take some action accordinglyI've also combed through the actual NavController
code, and what seems to be happening is that the NavController
is popping its internal backstack when back is clicked but not changing destinations because there's really no other destination to go to. It just seems like the NavController
is then in a weird inconsistent state where it and the currently displayed Fragment
are out of whack/not pointing to the same thing.
Has anyone else done this sort of migration with any success?
EDIT: I've created a repo that has a minimal project to recreate the crash I'm running into: https://github.com/alexsullivan114/ExampleNavigationCrashRepo. This repo kind of mimics the structure of the production app I'm working on. If you click into the slideshow drawer item then continue clicking on the text in the screens until you get to Next Example Fragment
then hit back a few times and click the text again you'll see the crash.
I'm sure the issue is in how I've setup the navigation library, I'm just not sure what the right way is without rewriting the rest of the apps navigation
As per the 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.
As explained in the Interact programmatically with Navigation, the app:defaultNavHost="true"
is calling setPrimaryNavigationFragment()
for you on the NavHostFragment
itself, but you aren't calling setPrimaryNavigationFragment()
on the parent fragment - your RootSlideshowFragment
. This means that none of the logic to automatically handle the back button correctly is being called - you've essentially broke the chain from the activity to the parent fragment to the NavHostFragment
. This causes you to intercept the back button even when there's no back stack (Navigation doesn't actually remove the last fragment when you pop it as otherwise it would not be present when your activity's exit animation is running).
Therefore to fix this, you need to:
OnBackPressedCallback
from RootSlideshowFragment
. It is not needed.MainActivity
, call setPrimaryNavigationFragment()
on the new Fragment: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
}