Fragments: rebuilding the internals

Introducing: the new state manager

Ian Lake
Android Developers

--

Fragments, more than most Android APIs, have evolved very organically over the years. They started as part of the Android platform itself, became a mirrored existence in the Android platform and as part of the Android Support Library, and now exist solely as part of Jetpack as AndroidX Fragments.

Note: you should under no circumstances use the Android framework version of Fragments. Besides being fully deprecated in Android 10, they weren’t receiving fixes for a considerable amount of time before that and, being baked into the framework, no backporting of fixes or consistency across devices and API levels can be expected.

While Architecture Components have taken over many of the roles that traditionally needed a Fragment (such as using a LifecycleObserver for Lifecycle callbacks or a ViewModel for retained state), if you’re using Fragments, you’re adding, removing, and interacting with them through a FragmentManager.

With Fragment 1.3.0-alpha08, some of the most significant restructuring of the internals of FragmentManager have been completed. This release swaps out much of the logic that used to live directly in FragmentManager with smaller, testable, and maintainable (internal) classes, the core of which is FragmentStateManager.

Note: I’m going to be talking a lot about the internals of FragmentManager in this post. TL/DR: please pay extra attention to regression testing with Fragment 1.3.0-alpha08 and file issues as soon as possible if you discover any regressions.

This new state manager is responsible for some pretty key parts of Fragments:

  • Moving Fragments through their lifecycle methods
  • Running animations and transitions
  • Handling postponed transactions

We’ve taken a ground up look at how those systems previously work, found them wanting, and rewrote them from scratch. They’re now better than ever, we were able to close out 10+ long standing related issues, and the internal restructuring has cleared the way to build support for multiple back stacks in a single FragmentManager and simplify the Fragment lifecycle.

FragmentManager’s moveToState()

Each FragmentManager is associated with a host. In the vast majority of cases for fragments, this is a FragmentActivity (there is an entire layer of FragmentController and FragmentHostCallback for building your own custom host, but let’s avoid that discussion here). As the activity moves to CREATED, STARTED, and RESUMED, FragmentManager dispatches those changes down to its fragments. This is the role of moveToState().

Of course, it isn’t quite that straightforward. There is a lot of conditional logic to determine exactly what state the fragment should be in — the activity lifecycle state (or the parent fragment’s state for nested fragments) is only the first part and serves as the maximum state the fragment can be in. This maximum is there to ensure that the lifecycle of the activity, fragments, and their child fragments are all properly nested.

So our first order of business in simplifying moveToState() was to collapse all of that logic into one place. Thus was born FragmentStateManager. Each fragment instance is tied to a FragmentStateManager under the hood. By introducing this class internally, we were able to take much of code that interacts with the fragment (such as calling the fragment’s onCreateView and other lifecycle methods) out of FragmentManager itself.

That split also allowed us to write a single method that would take all of the backward compatible required logic for what state the fragment should actually be in and centralize it in one place: computeExpectedState(). This one method keeps track of all of the current state and determines what state the fragment should be in. 98% of the time, it is the same state as the host / parent fragment, but that 2% makes a big difference to those apps built on fragments.

However, we ran into one case where we didn’t have a way to determine the right state: postponed fragments.

Postponed fragments

Fragments, for better or worse, inherited a lot of the same nomenclature and API surface as activities. Part of this inheritance was around transitions and the ability to postpone your enter transition until you’re ready. This is critical to shared element transitions (where you really want to have an image loaded to know its dimensions and position on the screen before starting the transition over to that location), but also allows you to ensure that more intensive loading calls don’t happen at the same time as your transition, avoiding jank.

A postponed fragment has two important qualities:

  1. Its view was created, but is not visible
  2. Its lifecycle is capped at STARTED

As soon as you call startPostponedEnterTransition(), the fragment’s transition would run, the view would become visible, and the fragment would be able to move to RESUMED. This is, in fact, exactly what the new state manager does, but it was not how Fragments worked before. To quote the Postponed Fragments leave the Fragments and FragmentManager in an inconsistent state bug:

When a Fragment is postponed using postponeEnterTransition(), the expected behavior is that the container the Fragment is added to does not run any enter animations or previously queued up exit animations (i.e., for a replace() operation) until the Fragment calls startPostponedEnterTransition(). It is also expected that the Fragment does not reach the RESUMED state while its container is postponed.

However, it seems like FragmentManager isn’t just doing that, but instead is moving the Fragment and the whole FragmentManager into a weird, inconsistent state.

Namely, any FragmentTransaction that touches the container of the postponed Fragment is ‘rolled back’ (i.e., done in reverse), but those Fragments aren’t actually moved to their proper state.

This led to a litany of issues:

Actually fixing any of these issues meant replacing the entire roll back process used by postponed fragments with a system that keeps the FragmentManager in a consistent, up to date state, while still maintaining the important qualities of postponed fragments.

Working at the container level

FragmentManager has this nice (read: handy, but not fun as the maintainer) property where it lets you pass in any container ID for where you want to place a Fragment. Even for a single FragmentTransaction, you can add a fragment to one container, remove another from a different container, replace a third container’s topmost fragment, etc. The rub comes in when it comes to animating in/out the fragments — something that happens solely at the container level.

Fragments support a number of animating systems:

  • The old and busted framework Animation API
  • The framework Animator API
  • The framework Transition API (only API 21+, also pretty busted)
  • The AndroidX Transition API

As you might know, naming is one of the hardest problems in Computer Science, so when we went to build a class that could control all of these APIs, it took a while to settle on SpecialEffectsController (this class isn’t part of the public API, so names are still subject to change, thankfully). This class exists on the container level and coordinates all of the “special effects” associated with entering and exiting fragments.

The SpecialEffectsController is the single source of truth on what should be happening to that container. This means that if the topmost added fragment is postponed, the entire container is postponed. There’s no more logic needed at the FragmentManager layer, nor any rollback of transactions (which, as we mentioned, can affect multiple containers). Thus, the FragmentManager is in the correct state and we still get all of the special properties of postponed fragments.

This base API then allowed us to centralize all of the crazy special effects APIs that fragment has into a single DefaultSpecialEffectsController that is responsible for running transitions and animations and animators. Again, moving logic that used to be scattered across FragmentManager into a single place.

So what does a ‘new state manager’ mean

Well, it means that instead of this architecture:

The old state manager: everything is in FragmentManager

It looks more like this:

The new state manager: FragmentManager talks to individual FragmentStateManager instances, which coordinate with other fragments in their container via SpecialEffectsController

By splitting up the internals of FragmentManager, the logic has been greatly simplified at each layer:

  • The FragmentManager only has state that applies to all fragments
  • The FragmentStateManager manages the state at the fragment level
  • The SpecialEffectsController manages the state at the container level

This separation of responsibilities has let us expand our test suite by almost 30%, covering many more scenarios that were near impossible to test in isolation.

Should I expect behavior changes?

No. In fact, we run a significant portion of the internal fragment tests against both the old and new state managers specifically to ensure we have a strong set of regression tests in place.

However, if you were relying on the inconsistent state a postponed fragment put the FragmentManager into, then yeah, you’ll find that you now actually get the correct state. You’ll find the list of bug fixes associated with the new state manager as part of the release notes, so take a look through that to make sure that your issues aren’t caused by your own workarounds for the old broken behavior that you can now just remove.

Similar to the changes to onDestroyView timing in Fragment 1.2.0, the new state manager will keep your fragment in the STARTED state until its transitions/animations/animators/special effects all finish, thus bringing consistency across all fragments, whether they are postponed directly or postponed because of other fragments in that same container.

What if I *do* see behavior changes?

After you upgrade to Fragment 1.3.0-alpha08, the new state manager is enabled by default. If you see differences in your app, the first step is to see if it is related to the new state manager by using the new experimental API:

FragmentManager.enableNewStateManager(false)

This API is an escape hatch back into the old world and lets you verify that any changes you’re seeing are tied to the new state manager. This unblocks you from upgrading the Fragment 1.3.0-alpha08 and lets you build a sample project that reproduces the issue that you can attach when you file an issue against Fragments.

Note: the FragmentManager.enableNewStateManager() API is experimental. That means that it is not considered part of the stable API surface of Fragments and can be removed at any point. Removing all of the old code is a significant code reduction, but given the importance of getting this right, we likely won’t remove the API until after the stable release of Fragment 1.3.0 — i.e., consider it on notice for removal in a Fragment 1.3.1 release.

With over 100 individual changes over an 11 month period, this is absolutely the largest internal change to Fragments in a while and sets us up for a much more maintainable, sustainable, and understandable code base. That means more consistent behavior across Fragments and a firm base you can rely on when building your app. We’d appreciate any help you can offer in making sure this new state manager is the best it can be by continuing to file issues and offering feedback.

--

--