Execute Fragment Transaction Again on Pop

A deep swoop into what really went into this feature

If a 'back stack' is a set of screens that you tin navigate back through via the organization back push, 'multiple dorsum stacks' is but a bunch of those, right? Well, that's exactly what we've done with the multiple back stack support added in Navigation 2.4.0-alpha01 and Fragment 1.four.0-alpha01!

The joys of the arrangement dorsum button

Whether you're using Android's new gesture navigation system or the traditional navigation bar, the ability for users to become 'dorsum' is a key part to the user experience on Android and doing that correct is an important part to making your app feel similar a natural part of the ecosystem.

In the simplest cases, the organization back button merely finishes your action. While in the past you might have been tempted to override the onBackPressed() method of your action to customize this beliefs, it is 2021 and that is totally unnecessary. Instead, there are APIs for custom back navigation in the OnBackPressedDispatcher. This is really the same API that FragmentManager and NavController already plug into.

That means when y'all apply either Fragments or Navigation, they utilise the OnBackPressedDispatcher to ensure that if you're using their back stack APIs, the system back push button works to reverse each of the screens that you've pushed onto the back stack.

Multiple back stacks doesn't change these fundamentals. The organization back button is notwithstanding a one directional command — 'go back'. This has a profound issue on how the multiple back stack APIs piece of work.

Multiple back stacks in Fragments

At the surface level, the back up for multiple dorsum stacks is deceptively straightforward, merely requires a bit of an explanation of what actually is the 'fragment back stack'. The FragmentManager'south back stack isn't fabricated upwardly of fragments, but instead is made upwards of fragment transactions. Specifically, the ones that have used the addToBackStack(String proper name) API.

This means when you commit() a fragment transaction with addToBackStack(), the FragmentManager is going to execute the transaction past going through and executing each of the operations (the replace, etc.) that you specified on the transaction, thus moving each fragment through to its expected state. FragmentManager and so holds onto that transaction as role of its back stack.

When you call popBackStack() (either direct or via FragmentManager'southward integration with the system back button), the topmost transaction on the fragment dorsum stack is reversed — an added fragment is removed, a hidden fragment is shown, etc. This puts the FragmentManager back into the same state that information technology was earlier the fragment transaction was initially committed.

Annotation: I cannot stress this plenty, only you lot absolutely should never interleave transactions with addToBackStack() and transactions without in the same FragmentManager: transactions on your back stack are blissfully unaware of non-back stack changing fragment transactions — swapping things out from underneath those transactions makes that reversal when yous pop a much more dicey proffer.

This means that popBackStack() is a destructive performance: whatever added fragment volition accept its state destroyed when that transaction is popped. This means you lose your view state, any saved instance land, and any ViewModel instances yous've attached to that fragment are cleared. This is the primary difference between that API and the new saveBackStack(). saveBackStack() does the same reversal that popping the transaction does, but it ensures that the view country, saved example state, and ViewModel instances are all saved from devastation. This is how the restoreBackStack() API tin can later on recreate those transactions and their fragments from the saved state and finer 'redo' everything that was saved. Magic!

This didn't come without paying downwards a lot of technical debt though.

Paying down our technical debts in Fragments

While fragments have e'er saved the Fragment's view country, the just time that a fragment's onSaveInstanceState() would exist called would exist when the Activeness's onSaveInstanceState() was chosen. To ensure that the saved case land is saved when calling saveBackStack(), we demand to too inject a phone call to onSaveInstanceState() at the correct point in the fragment lifecycle transitions. We can't call information technology too soon (your fragment should never have its state saved while information technology is still STARTED), just non too late (you want to save the state before the fragment is destroyed).

This requirement kicked off a process to fix how FragmentManager moves to country to make certain there'southward one place that manages moving a fragment to its expected country and handles re-entrant behavior and all the state transitions that go into fragments.

35 changes and half-dozen months into that restructuring of fragments, information technology turned out that postponed fragments were seriously cleaved, leading to a world where postponed transactions were left floating in limbo — non actually committed and not actually not committed. Over 65 changes and another 5 months later, and nosotros had completely rewritten near of the internals of how FragmentManager manages state, postponed transitions, and animations. That endeavor is covered in more particular in my previous weblog post:

What to expect in Fragments

With the technical debt paid downward (and a much more reliable and understandable FragmentManager), the tip of the iceberg APIs of saveBackStack() and restoreBackStack() were added.

If you don't employ these new APIs, nothing changes: the single FragmentManager back stack works as earlier. The existing addToBackStack() API remains unchanged — yous can utilise a null proper noun or any proper noun you want. Notwithstanding, that proper name takes on a new importance when you start looking at multiple back stacks: it is that proper name that is the unique key for that fragment transaction that yous'd apply with saveBackStack() and subsequently with restoreBackStack().

This might be easier to see in an example. Let'south say that you have added an initial fragment to your activity, so washed two transactions, each with a single replace operation:

          // This is the initial fragment the user sees
fragmentManager.commit {
setReorderingAllowed(true)
replace<HomeFragment>(R.id.fragment_container)
}
// Afterward, in response to user actions, we've added ii more than
// transactions to the back stack
fragmentManager.commit {
setReorderingAllowed(true)
supercede<ProfileFragment>(R.id.fragment_container)
addToBackStack("profile")
}
fragmentManager.commit {
setReorderingAllowed(true)
supplant<EditProfileFragment>(R.id.fragment_container)
addToBackStack("edit_profile")
}

This means that our FragmentManager looks like:

FragmentManager state after three commits

Permit's say that we want to bandy out our profile back stack and swap to the notifications fragment. We'd call saveBackStack() followed past a new transaction:

          fragmentManager.saveBackStack("profile")          fragmentManager.commit {
setReorderingAllowed(true)
supplant<NotificationsFragment>(R.id.fragment_container)
addToBackStack("notifications")
}

Now our transaction that added the ProfileFragment and the transaction that added the EditProfileFragment has been saved nether the "contour"
key. Those fragments have had their land saved completely and FragmentManager is holding onto their state aslope the transaction state. Chiefly: those fragment instances no longer be in memory or in the FragmentManager — it is just the state (and any not config land in the class of ViewModel instances):

FragmentManager state after nosotros've saved the profile dorsum stack and added one more than commit

Swapping back is simple enough: we tin do the same saveBackStack() operation on our "notifications" transaction and then restoreBackStack():

          fragmentManager.saveBackStack("notifications")          fragmentManager.restoreBackStack("profile")        

The ii stacks accept finer swapped positions:

FragmentManager state after swapping the two stacks

This style of maintaining a single agile back stack and swapping transactions onto it ensures that the FragmentManager and the rest of the organization always has a consistent view of what actually is supposed to happen when the system back push is tapped. In fact, that logic remained entirely unchanged: it still just pops the last transaction off of the fragment back stack like before.

These APIs are purposefully minimal, despite their underlying effects. This makes it possible to build your own structure on elevation of these building blocks while fugitive whatsoever hacks to save Fragment view state, saved instance state, and not config land.

Of class, if you don't want to build your own structure on top of these APIs, you tin can also use the i we provide.

Bringing multiple back stacks to any screen type with Navigation

The Navigation Component was built from the beginning as a generic runtime that knows null about Views, Fragments, Composables, or any other type of screen or 'destination' you lot might implement within your action. Instead, it is the responsibleness of an implementation of the NavHost interface to add one or more Navigator instances that do know how to interact with a particular type of destination.

This meant that the logic for interacting with fragments was entirely encapsulated in the navigation-fragment artifact and its FragmentNavigator and DialogFragmentNavigator. Similarly the logic for interacting with Composables is in the completely independent navigation-etch artifact and its ComposeNavigator. That brainchild means that if you want to build your app solely with Composables, you are not forced to pull in whatever dependency on fragments when y'all use Navigation Compose.

This level of separation means that at that place are really two layers to multiple back stacks in Navigation:

  • Saving the state of the individual NavBackStackEntry instances that make upwardly the NavController dorsum stack. This is the responsibility of the NavController.
  • Saving any Navigator specific state associated with each NavBackStackEntry (eastward.grand., the fragment associated with a FragmentNavigator destination). This is the responsibility of the Navigator.

Special attention was given to the cases where the Navigator has not been updated to support saving its state. While the underlying Navigator API was entirely rewritten to support saving state (with new overloads of its navigate() and popBackStack() APIs that y'all should override instead of the previous versions), NavController will relieve the NavBackStackEntry country even if the Navigator has non been updated (backward compatibility is a big deal in the Jetpack world!).

PS: this new Navigator API also makes it mode easier to test your own custom Navigator in isolation by attaching a TestNavigatorState that acts as a mini-NavController.

If you lot're just using Navigation in your app, the Navigator level is more of an implementation detail than something yous'll ever demand to collaborate with directly. Suffice it to say, we've already done the work required to get the FragmentNavigator and the ComposeNavigator over to the new Navigator APIs then that they correctly save and restore their state; there'south no work yous demand to exercise at that level.

Enabling multiple dorsum stacks in Navigation

If you're using NavigationUI, our set up of opinionated helpers for connecting your NavController to Textile view components, y'all'll find that multiple back stacks is enabled by default for card items, BottomNavigationView (and now NavigationRailView!), and NavigationView. This means that the common combination of using navigation-fragment and navigation-ui will just piece of work.

The NavigationUI APIs are purposefully built on top of the other public APIs available in Navigation, ensuring that you can build your own versions for precisely your set of custom components you want. The APIs to enable saving and restoring a back stack are no exception to this, with new APIs on NavOptions, the navOptions Kotlin DSL, in the Navigation XML, and in an overload for popBackStack() that let you specify that you desire a pop operation to salvage land or you want a navigate operation to restore some previously saved state.

For example, in Compose, whatsoever global navigation design (whether it is a bottom navigation bar, navigation rails, drawer, or anything yous tin dream up) tin can all employ the aforementioned technique as we evidence for integrating with BottomNavigation and call navigate() with the saveState and restoreState attributes:

          onClick = {
navController.navigate(screen.route) {
// Pop up to the start destination of the graph to
// avoid building up a large stack of destinations
// on the back stack as users select items
popUpTo(navController.graph.findStartDestination().id) {
saveState = true
}

// Avoid multiple copies of the same destination when
// reselecting the same item
launchSingleTop = true

// Restore state when reselecting a previously selected item
restoreState = truthful
}
}

Save your state, salvage your users

One of the most frustrating things for a user is losing their state. That's one of the reasons why fragments have a whole folio on saving state and one of the many reasons why I am so glad to get each layer updated to support multiple back stacks:

  • Fragments (i.due east., without using the Navigation Component at all): this is an opt-in change by using the new FragmentManager APIs of saveBackStack and restoreBackStack.
  • The core Navigation Runtime: adds opt-in new NavOptions methods for restoreState and saveState and a new overload of popBackStack() that likewise accepts a saveState boolean (defaults to false).
  • Navigation with Fragments: the FragmentNavigator now utilizes the new Navigator APIs to properly interpret the Navigation Runtime APIs into the Fragment APIs by using the Navigation Runtime APIs.
  • NavigationUI: The onNavDestinationSelected(), NavigationBarView.setupWithNavController(), and NavigationView.setupWithNavController() now utilise the new restoreState and saveState NavOptions past default whenever they would popular the back stack. This means that every app using those NavigationUI APIs will become multiple back stacks without any lawmaking changes on their role after upgrading the Navigation 2.4.0-alpha01 or higher.

If you'd like to await at some more examples that utilize this API, take a look at the NavigationAdvancedSample (newly updated without any of the NavigationExtensions code it used to require to back up multiple dorsum stacks):

And for Navigation Compose, consider looking at Tivi:

If you do meet any issues, please make sure to apply the official issue tracker to file bugs confronting Fragments or Navigation and we'll be certain to have a look at them!

harveybansta.blogspot.com

Source: https://medium.com/androiddevelopers/multiple-back-stacks-b714d974f134

0 Response to "Execute Fragment Transaction Again on Pop"

Post a Comment

Iklan Atas Artikel

Iklan Tengah Artikel 1

Iklan Tengah Artikel 2

Iklan Bawah Artikel