Fragment Manager

kgleong edited this page Sep 3, 2015 · 17 revisions

Fragment Manager

Reusing fragments in the Backstack

Question:

  • is it recommended to reuse fragments in the backstack if they need to be displayed on screen again?

Scenario:

  • User performs actions that causes fragments to be swapped in and out of a common ViewGroup container.
  • Fragment 1, then Fragment 2, then Fragment 3 were added then removed from the screen.
  • All fragment transactions use the replace() action, and save the resulting transactions to the backstack.
  • The backstack now looks like this:
    • 2: Transaction that removed Fragment 2 and added Fragment 3
    • 1: Transaction that removed Fragment 1 and added Fragment 2
    • 0: Transaction that added Fragment 1
  • User then navigates back to Fragment 1.
  • Rather than creating a new instance of Fragment 1, is there a way to retrieve it off the backstack to save memory and CPU cycles?

Answer:

  • It appears to be safe to use the findFragmentByTag function in a fragment manager to retrieve fragment instances that can be reused.
  • findFragmentByTag searches through all fragments that are still present within the activity, and if no match is found it searches through all fragments referenced by the backstack.

NOTE:

  • I have not done a complete review of the Android framework to rule out any gotchas that may occur from following this approach.
  • Hence, the "appears" caveat. It might help to look at FragmentPageAdapter to see how this is handled, but I suspect that it probably does use the lookup function mentioned above.

See below for the rationale behind the suggested approach

Rationale behind suggested approach

Answering this question required getting answers to the questions listed below. Questions below are best read in the order listed.

  1. What does findFragmentByTag do?
    • Searches through all fragments in an activity by tag and returns the first match.
    • This function first searches through all fragments that have been added and are still added to the activity.
      • This initial search looks through the mAdded fragment list.
    • Then it searches through all fragments that are sitting in the backstack if no match is found.
      • This subsequent search looks through the mActive fragment list.
  2. What are the mAdded and mActive lists?
    • mAdded:
      • Contains fragments that have been added and not removed or detached from the activity.
      • These fragments are privileged in the sense that they can:
        • Respond to events such as:
          • low memory events
          • configuration changes
        • Display custom menus and respond to menu item selections.
    • mActive:
      • A superset of mAdded that includes all fragments referenced by any FragmenTransaction objects in the backstack.
      • Fragments that are NOT in mAdded will not be able to respond to events or display custom menus.
    • What events modify these two fragment lists?
      • mAdded
        • a fragment is added to this list if the fragment is added to the activity.
        • a fragment is removed from this list if:
          1. the fragment is removed from the activity.
          2. the fragment is detached from the activity.
      • mActive
        • a fragment is added to this list if the fragment is added to the activity.
        • a fragment is removed from this list ONLY if the fragment is removed from an activity, and there are no longer any other references to it in the backstack.
  3. What does the moveToState() method do?
    • This method is crucial to understanding fragment lifecycle states.
  4. Is it safe to reuse a fragment from the backstack?
    • reusing a fragment from the backstack appears to be safe because of the conditions that must be met for a fragment to be removed from the mActive list.

Fragment Lookup

First, it's important to understand what fragmentManager.findFragmentByTag() actually does.

  1. mAdded and mActive are of type ArrayList<Fragment> and are member variables in FragmentManager.
  2. The function first searches for a fragment with a matching tag in reverse order in member variable mAdded.
  3. If no fragment is found it searches mActive in the same way.
  4. Returns the first encountered match.

The reverse order searching is an attempt to cut down on the run time since it's most likely that more recently added fragments are in higher demand than fragments introduced a while ago.

Here's the source from FragmentManager.java:

    // In FragmentManagerImpl

    public Fragment findFragmentByTag(String tag) {
        if (mAdded != null && tag != null) {
            for (int i = mAdded.size() - 1; i>=0; i--) {
                Fragment f = mAdded.get(i);
                if (f != null && tag.equals(f.mTag)) {
                    return f;
                }
            }
        }
        if (mActive != null && tag != null) {
            for (int i = mActive.size() - 1; i>=0; i--) {
                Fragment f = mActive.get(i);
                if (f != null && tag.equals(f.mTag)) {
                    return f;
                }
            }
        }
        return null;
    }

mAdded and mActive Fragment Lists

mAdded and mActive are member variables of FragmentManager instances and are of type ArrayList<Fragment>.

There is little documentation on the purpose of these two fields, so understanding exactly what they are and what they do requires a deep look at the FragmentManager and FragmentTransaction classes.

An overview of these two lists was posted as a response to a Stack Overflow question.

mAdded
  • Overview

    • Contains fragments that have been added and not removed or detached from the activity.
  • Purpose

    • The mAdded list is used to filter fragments that should respond to some select activity lifecycle events.
      • These fragments can be thought of as privileged in a sense.
      • The fragment lifecyle responds to the lifecyle events of its host activity. This is because a fragment cannot stand on its own; to be displayed on screen it must be attached to an activity and it's view heirarchy must follow suit as well.
    • The ONLY activity lifecycle events that are passed to mAdded fragments and not to the entire mActive fragment list are:
      • onLowMemory()
      • onConfigurationChanged()
      • onMenuItemSelected()
      • onCreatePanelMenu()
      • onPanelClosed()
  • Events that update mAdded

    • Adds elements to mAdded:
      • a fragment is added to the activity
    • Removes fragments from mAdded:
      • a fragment is removed from the activity
      • a fragment is detached from the activity
mActive
  • Overview

    • A fragment list superset of mAdded that includes all fragments referenced by any FragmenTransaction objects in the backstack.
    • Any fragments that are NOT in mAdded but in mActive will not be able to respond to events or display custom menus.
  • Purpose

The mActive fragment list serves two purposes:

  1. It is a catalog for all fragments that are tied to an activity. This is important for the following reasons:
    • When an activity needs to save its state, it must save the state of all of its fragments. Since the mActive list contains all fragments, when serializing, the framework only needs to store this list. The mAdded list is serialized as an array of integers, which are indexes into the mActive list.
    • When restoring the state of an activity, the fragment manager state must also be restored. The mActive list is used to restore the mAdded fragment list, since the serialized mAdded list is simply a list of integer indices.
    • As an activity goes through its lifecycle, it needs bring all of its fragments along through their respective lifecycles.
      • The fragment manager for the activity is responsible for bringing all fragments to the correct state. This involves inflating view hierarchies for fragments that are visible on screen and calling the correct lifecycle methods on each fragment.
      • To ensure completeness, the fragment manager will look at all fragments in mActive, not just those in mAdded.
  2. It holds all fragment objects that the backstack refers to.
    • This allows lookups to be performed on all fragments contained in the backstack history.
  • Events that update mActive
    • Adds elements to mActive:
      • a fragment is added to the activity
    • Removes fragments from mActive:
      • when a fragment is removed from an activity, and there are no longer any other references to it in the backstack, the fragment is removed from mActive.
Supporting Documentation

See comments added in each code block. All code snippets are from the Android platform_frameworks_support repo

  • What events cause updates to mAdded?

  • What events cause updates to mActive?

  • Activities only use mAdded when passing selected callbacks down to its fragments

  • Actvities use mActive as they move through their major lifecycle callbacks

fragmentManager.moveToState

The fragmentManager.moveToState() method is an important method to understand because it handles all fragment state changes.

Any FragmentTransaction action, such as add, remove, detach, etc., eventually calls this method to get the fragment into the correct state.

Below are two method signatures that will be covered:

  1. void moveToState(int newState, boolean always)
    • This method attempts to move all fragments in mActive to newState.
  2. void moveToState(Fragment f, int newState, int transit, int transitionStyle, boolean keepActive)
    • Base method that all other moveToState method signatures eventually call.
    • Handles all fragment state changes and performs the following:
      • ensures state changes are valid and disallows invalid state changes.
      • ensures that state changes proceed through any intermediary states if necessary
      • performs any operations that are associated with changes in state. E.g., creating view hierarchies and calling lifecycle callback methods.

Below is the first moveToState() method. From FragmentManagerImpl (full source):

    // the "always" parameter is an override flag that forces execution if mCurState == newState.
    // usually false is passed in.
    void moveToState(int newState, boolean always) {

        // See below for this method implementation.
        moveToState(newState, 0, 0, always);
    }

    void moveToState(int newState, int transit, int transitStyle, boolean always) {
        ...
        // Moves all fragments in the activity (i.e., mActive) to newState.
        for (int i = 0; i < mActive.size(); i++) {
            Fragment f = mActive.get(i);

            if (f != null) {
                // Last parameter specifies whether the
                // fragment should be retained in the mActive list.
                moveToState(f, newState, transit, transitStyle, false);
                ...
            }
        }
        ...
    }

this moveToState(int newState, boolean always) method is used by the host activity to bring all of its fragments to the correct state as the activity goes through its own lifecycle. This is important when an activity's state is being restored by the operating system.

The code excerpt below shows the activity lifecycle methods onCreate() and onStart() from FragmentActivity, which is the base activity class for any activity. Below it are the corresponding calls into the FragmentManaer, where the call to moveToState() is made.

onCreate() has been included since this method restores the state of the activity's fragment manager as well. Included in this restoration are the mActive and mAdded fragment lists discussed above.

In FragmentActivity (full source):

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        ...
        super.onCreate(savedInstanceState);

        NonConfigurationInstances nc = (NonConfigurationInstances)
                getLastNonConfigurationInstance();
        ...
        if (savedInstanceState != null) {
            Parcelable p = savedInstanceState.getParcelable(FRAGMENTS_TAG);

            // The mFragments field name is a bit misleading.  mFragments is actually the
            // FragmentManager for this activity.
            //
            // restoreAllState() looks at the saved instance state and restores the
            // FragmentManager state into mFragments.
            //
            // Important fragment lists that are restored by restoreAllState():
            // mFragments.mAdded
            // mFragments.mActive
            mFragments.restoreAllState(p, nc != null ? nc.fragments : null);
        }
        // Puts all fragments into the CREATE state
        mFragments.dispatchCreate();
    }

    @Override
    protected void onStart() {
        ...
        if (!mCreated) {
            mCreated = true;
            mFragments.dispatchActivityCreated();
        }
        ...
        mFragments.dispatchStart();
        ...
    }

And the corresponding dispatch* FragmentManager calls, which call moveToState() (full source):

    public void dispatchCreate() {
        mStateSaved = false;

        // This moves all fragments in mActive to the CREATED state
        moveToState(Fragment.CREATED, false);
    }

    public void dispatchActivityCreated() {
        mStateSaved = false;
        moveToState(Fragment.ACTIVITY_CREATED, false);
    }

    public void dispatchStart() {
        mStateSaved = false;
        moveToState(Fragment.STARTED, false);
    }

As mentioned above, this moveToState call loops through each fragment in mFragments.mActive and calls the base moveToState method that handles all fragment state changes.

Fragment States

    // from Fragment

    static final int INITIALIZING = 0;     // Not yet created.
    static final int CREATED = 1;          // Created.
    static final int ACTIVITY_CREATED = 2; // The activity has finished its creation.
    static final int STOPPED = 3;          // Fully created, not started.
    static final int STARTED = 4;          // Created and started, not resumed.
    static final int RESUMED = 5;          // Created started and resumed.

The signature for the base method is:

  • void moveToState(Fragment f, int newState, int transit, int transitionStyle, boolean keepActive)

This is a lengthy method, but in summary it:

  • has two separate flows, one for when a fragment state is promoted (goes to a higher state value) and one when the state is demoted (goes to a lower state value).
  • ensures the fragment moves through each state on the way from the start to end state.
  • calls the appropriate fragment lifecycle callback methods as the fragment moves through states.
  • removes the fragment from the mActive list when a fragment state is set to the Fragment.INITIALIZING state.

Below is an abbreviated version of the method from FragmentManagerImpl (full source):

    void moveToState(Fragment f, int newState, int transit, int transitionStyle, boolean keepActive) {
        // Forces any fragment that has been removed or detached to stay in the CREATED state.
        if ((!f.mAdded || f.mDetached) && newState > Fragment.CREATED) {
            newState = Fragment.CREATED;
        }
        ...
        // Fragment is moving forward in it's lifecycle state.
        if (f.mState < newState) {
            ...
            switch (f.mState) {
                case Fragment.INITIALIZING:
                    ...
                    f.onAttach(mActivity);
                    ...
                    if (!f.mRetaining) {
                        // performCreate() calls onCreate()
                        f.performCreate(f.mSavedFragmentState);
                    }
                    // Create view hierarchy if fragment has been statically
                    // added to the activity XML layout file.
                    ...
                case Fragment.CREATED:
                    if (newState > Fragment.CREATED) {
                        // Create view hierarchy if fragment is dynamically added.
                        ...
                    }
                case Fragment.ACTIVITY_CREATED:
                case Fragment.STOPPED:
                    if (newState > Fragment.STOPPED) {
                        // performStart() calls onStart()
                        f.performStart();
                    }
                case Fragment.STARTED:
                    if (newState > Fragment.STARTED) {
                        // performResume() calls onResume()
                        f.performResume();
                        ...
                    }
            }
        }
        // Fragment is moving backwards in lifecycle state.
        else if (f.mState > newState) {
            switch (f.mState) {
                case Fragment.RESUMED:
                    if (newState < Fragment.RESUMED) {
                        f.performPause();
                        ...
                    }
                case Fragment.STARTED:
                    if (newState < Fragment.STARTED) {
                        f.performStop();
                    }
                case Fragment.STOPPED:
                    if (newState < Fragment.STOPPED) {
                        f.performReallyStop();
                    }
                case Fragment.ACTIVITY_CREATED:
                    if (newState < Fragment.ACTIVITY_CREATED) {
                        // performDestroyView() calls onDestroyView()
                        f.performDestroyView();
                        ...
                    }
                case Fragment.CREATED:
                    if (newState < Fragment.CREATED) {
                        ...
                        if (!f.mRetaining) {
                            // performDestroy() calls onDestroy()
                            f.performDestroy();
                        }

                        ...
                        f.onDetach();
                        ...

                        // keepActive is a flag that forces a fragment to remain in
                        // the mActive list.  Normally this is false.
                        if (!keepActive) {

                            // Retained fragments should stay in the mActive list.
                            if (!f.mRetaining) {

                                // This removes the fragment from the mActive list.
                                makeInactive(f);
                            }
                            ...
                        }
                    }
                }
            }
        }

        f.mState = newState;
    }

Note that in the code above, none of the switch statements contain a break command.

Therefore, whenever a fragment is moving to a different state, it performs all operations associated with all the states between the starting state f.mState and the end state newState.

All associated lifecycle callback methods are called as appropriate. For example, if a fragment is in the Fragment.RESUMED state and it's being taken down to the Fragment.CREATED state, which is what happens when you remove a fragment that's in the backstack, it first calls f.performPause() and that calls f.onPause().

Fragment removed from mActive list

In the second switch statement, where the fragment is being moved to a lower state, there's a makeInactive call that removes the fragment from the mActive list.

This only happens if the newState is set to Fragment.INITIALIZING, which is the initial default state for a fragment when it is first instantiated.

When is a fragment's state set to initializing other than during instantiation?

The only place that this occurs is in fragmentManager.removeFragment() (full source).

    public void removeFragment(Fragment fragment, int transition, int transitionStyle) {
        // Checks whether the fragment is referenced by any transactions in the backstack.
        //
        // Each fragment has a counter called backStackNesting that is incremented and
        // decremented anytime a fragment is added or popped off the backstack respectively.
        //
        // if fragmentTransaction.addToBackstack() is not called, then the counter is not
        // incremented.
        final boolean inactive = !fragment.isInBackStack();

        if (!fragment.mDetached || inactive) {
            ...
            // If the fragment is not in the backstack, set the state to INITIALIZING.
            moveToState(fragment, inactive ? Fragment.INITIALIZING : Fragment.CREATED,
                    transition, transitionStyle, false);
        }
    }
When is a fragment removed?

There are a few scenarios when a fragment is removed:

  • fragmentTransaction.remove() (full source)
  • fragmentTransaction.replace() (full source)
  • fragmentTransaction.popFromBackStack() (full source)
    • if a fragment was added in the transaction then the opposite action (remove) is called. Similarly for when a fragment replaces another fragment.

Is it safe to reuse a fragment from the backstack?

It would appear that it is safe to reuse a fragment in the backstack. The only time the Android framework removes a fragment from the mActive list is when the fragment is no longer in the backstack and when it is being removed from view.

Clone this wiki locally
You can’t perform that action at this time.
You signed in with another tab or window. Reload to refresh your session. You signed out in another tab or window. Reload to refresh your session.
Press h to open a hovercard with more details.