1. Introduction

Fragment is a very old guy, its first commit was recorded in 2010, and its last commit was recorded on June 11, 2021. It’s 11 years old, but it’s still going strong, and it shines in Jetpack. Navigation component is a Fragment based jump component. Google’s single Activity project structure is one Activity and multiple Fragments. For years now, the first thing that comes to mind when you think of A Fragment is tablet app development, and it hasn’t been used very often in mobile projects. On the one hand, mobile phone projects are generally implemented with multi-activity structure, which does not involve so many fragments. On the other hand, fragments are also more complex than activities, which is a bit troublesome to use, even painful, and difficult to locate problems. Therefore, in the selection of technology, we should avoid them if possible. It’s so hard to use that Piwai, the Square team’s author of LeakCanary, published a 2014 article advocating against the use of Fragments. In any case, over the years, the Android team hasn’t given up support for Fragments, and even in Jetpack, they’re starting to play an increasingly important role. Navigation has been promoted more recently, and I’ve been working on a tablet project, Therefore, the source code of the Fragment main process was studied comprehensively. We have gained a lot and believe that it will be of great benefit to Navigation research in the future.

2. Create fragments

Write a simple Demo according to the official documentation. Learn about the use of fragments.

2.1 Creating a Fragment Class

class ExampleFragment extends Fragment { public ExampleFragment() { super(R.layout.example_fragment); }}Copy the code

2.2 Adding Fragments to your Activity

Adding a Fragment to an Activity can be done either as an XML file or as code programming. This article covers only the second implementation.

<! -- res/layout/example_activity.xml --> <androidx.fragment.app.FragmentContainerView xmlns:android="http://schemas.android.com/apk/res/android" android:id="@+id/fragment_container_view" android:layout_width="match_parent" android:layout_height="match_parent" />Copy the code
public class ExampleActivity extends AppCompatActivity { public ExampleActivity() { super(R.layout.example_activity); } @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); if (savedInstanceState == null) { getSupportFragmentManager().beginTransaction() .setReorderingAllowed(true) .add(R.id.fragment_container_view, ExampleFragment.class, null) .commit(); }}}Copy the code

As you can see, the FragmentManager is the main way to add fragments to the fragment_container_View layout. The Demo is pretty simple, with template code that you can copy online. What FragmentManager, Transaction, and Commit () mean, however, remains to be explored.

3. Deeply understand the Fragment principle

3.1 What is the nature of Fragment

3.1.1 Fragment is essentially a View

As we saw in the previous DEMO, fragments can be added to FragmentContainerView. Any component that can be added to a ViewGroup must also be a View or ViewGroup. Source code as proof:

//androidx.fragment.app.Fragment.java

ViewGroup mContainer;

// The View generated for this fragment.
View mView;
Copy the code

AddView () and removeView() are easier and more intuitive to use.

Of course, Fragment is a View in nature, but it is not limited to that. In real projects, interfaces and business logic are often very complex, and they often need to deal with various UI state changes, such as adding a group of views, replacing a group of views, deleting a group of views, and cooperating with activities. In short, it would be much more difficult for developers to implement this logic themselves than using fragments. Even though fragments look complicated, they are much simpler than trying to implement the same functionality ourselves.

3.1.2 Fragment nature is not limited to Views

Fragment as a View can be added to the layout of an Activity. But it is more powerful and has the following characteristics:

  1. The processing life cycle is more complex than the Activity life cycle
  2. Operate multiple fragments using FragmentTransaction
  3. Maintain a rollback stack through the FragmentManager to fall back to the interface of the previous FragmentTransaction operation
//androidx.fragment.app.Fragment.java

static final int INITIALIZING = -1;          // Not yet attached.
static final int ATTACHED = 0;               // Attached to the host.
static final int CREATED = 1;                // Created.
static final int VIEW_CREATED = 2;           // View Created.
static final int AWAITING_EXIT_EFFECTS = 3;  // Downward state, awaiting exit effects
static final int ACTIVITY_CREATED = 4;       // Fully created, not started.
static final int STARTED = 5;                // Created and started, not resumed.
static final int AWAITING_ENTER_EFFECTS = 6; // Upward state, awaiting enter effects
static final int RESUMED = 7;                // Created started and resumed.
Copy the code

As we can see from the code, there are eight states. There are two categories, view-related and activity-related. This will be explained in detail later, but I will stop here.

3.2 FragmentTransaction

3.2.1 FragmentTransaction overview

A call database course in software engineering, database has a name is the concept of “transaction”, which represents the multiple operation is atomic, either successful or fail together, a typical example is the transfer, zhang SAN li si transfer 100 yuan so to a transaction, there are two operations, to deduct $100 from zhang SAN’s account, Add 100 yuan to Li Si’s account. Because of transactions, there is no situation where one operation succeeds and the other fails, or the consequences would be disastrous.

SharedPreference also has a commit method in Android.

SharedPreferences sharedPreferences= getSharedPreferences("data",Context.MODE_PRIVATE); SharedPreferences.Editor editor = sharedPreferences.edit(); Editor. PutString (" name ", "Tom"); editor.putInt("age", 28); editor.putBoolean("marrid",false); editor.commit();Copy the code

PutString, putInt, putBoolean.

FragmentTransaction is similar to SharedPreference. It can operate on multiple fragments simultaneously.

Suppose you have a layout file like this:

<LinearLayout android:orientation="vertical">
    
    <ViewGroup1>
        <Fragment1 />
    </ViewGroup1>
    
    <ViewGroup2>
        <Fragment2 />
    </ViewGroup2>
    
    <ViewGroup3>
        <Fragment3 />
    </ViewGroup3>
    
</LinearLayout>
Copy the code

Suppose we want to add Fragment1 to ViewGroup1, replace ViewGroup2 with Fragment2, and remove the Fragment from ViewGroup3.

The pseudocode is as follows:

getSupportFragmentManager().beginTransaction()
    .setReorderingAllowed(true)
    .add(ViewGroup1, Fragment1.class, null)
    .replace(ViewGroup2,Fragment2.class,null)
    .remove(Fragment3)
    .commit();
Copy the code

Q: Why do I put these three operations in one transaction? Can’t I do all three in a row?

A: In some cases yes, in some cases no. When it comes to rolling back the stack, pressing the back key makes a big difference whether you put it in a transaction or not. We will name the three operations OP1, OP2, and OP3. If call FragmentTransaction. AddToBackStack (), three operations in a transaction, press the return key when they perform with OP1, OP2, OP3 contrary operation, That is, ViewGroup1, ViewGroup2, and ViewGroup3 are restored to the state before the transaction. If the three operations are not placed in a transaction, the state of ViewGroup3, ViewGroup2, and ViewGroup1 will be restored in sequence

3.2.2 FragmentTransaction source code analysis

3.2.2.1 Supported Operation OP_CMD

//FragmentTransaction.java
static final int OP_NULL = 0;
static final int OP_ADD = 1;
static final int OP_REPLACE = 2;
static final int OP_REMOVE = 3;
static final int OP_HIDE = 4;
static final int OP_SHOW = 5;
static final int OP_DETACH = 6;
static final int OP_ATTACH = 7;
static final int OP_SET_PRIMARY_NAV = 8;
static final int OP_UNSET_PRIMARY_NAV = 9;
static final int OP_SET_MAX_LIFECYCLE = 10;
Copy the code

Pick something you can read at a glance.

  1. OP_ADD Adds the Fragment
  2. OP_REPLACE Replaces the Fragment on a ViewGroup
  3. Delete OP_REMOVE fragments
  4. OP_HIDE Hidden Fragment, equivalent to view.setvisibility (view.gone)
  5. OP_SHOW Displays fragments, equal to view.setvisibility (view.visible)
  6. OP_DETACH detach Fragment
  7. OP_ATTACH attach Fragment

The corresponding methods are as follows:

//FragmentTransaction.java

public FragmentTransaction add(@NonNull Fragment fragment, @Nullable String tag)  {
    doAddOp(0, fragment, tag, OP_ADD);
    return this;
}

public FragmentTransaction replace(@IdRes int containerViewId, @NonNull Fragment fragment) {
    return replace(containerViewId, fragment, null);
}

public FragmentTransaction remove(@NonNull Fragment fragment) {
    addOp(new Op(OP_REMOVE, fragment));

    return this;
}
Copy the code

3.2.2.2 OP class

public abstract class FragmentTransaction { ArrayList<Op> mOps = new ArrayList<>(); static final class Op { int mCmd; Fragment mFragment; int mEnterAnim; int mExitAnim; int mPopEnterAnim; int mPopExitAnim; Lifecycle.State mOldMaxState; Lifecycle.State mCurrentMaxState; Op() { } Op(int cmd, Fragment fragment) { this.mCmd = cmd; this.mFragment = fragment; this.mOldMaxState = Lifecycle.State.RESUMED; this.mCurrentMaxState = Lifecycle.State.RESUMED; } Op(int cmd, @NonNull Fragment fragment, Lifecycle.State state) { this.mCmd = cmd; this.mFragment = fragment; this.mOldMaxState = fragment.mMaxState; this.mCurrentMaxState = state; }}}Copy the code

As you can see from the source code, there is ArrayList<Op> mOps in FragmentTransaction. It indicates that several operations need to be processed in a transaction. The two most important fields of the OP class are mCmd and mFragment. Indicates that the specified operation is performed on a Fragment.

3.2.2.3 FragmentTransaction.com MIT () method

//FragmentTransaction.java
public abstract class FragmentTransaction {
    public abstract int commit();
}
Copy the code

We see that the commit() method is an abstract method. It is implemented by the BackStackRecord class.

final class BackStackRecord extends FragmentTransaction implements FragmentManager.BackStackEntry, FragmentManager.OpGenerator { @Override public int commit() { return commitInternal(false); }}Copy the code

3.3 BackStackRecord

3.3.1 BackStackRecord overview

BackStackRecord is a subclass of FragmentTransaction, meaning that it can operate on multiple fragments at the same time. And, as the name implies, it goes into the BackStack. The rollback stack, defined in the FragmentManager, is a collection of ArrayLists.

//FragmentManager.java
ArrayList<BackStackRecord> mBackStack;
Copy the code

In short, BackStackRecord supports transactions while being placed on a rollback stack

3.3.2 BackStackRecord Commit process

int commitInternal(boolean allowStateLoss) {
    if (mCommitted) throw new IllegalStateException("commit already called");
    if (FragmentManager.isLoggingEnabled(Log.VERBOSE)) {
        Log.v(TAG, "Commit: " + this);
        LogWriter logw = new LogWriter(TAG);
        PrintWriter pw = new PrintWriter(logw);
        dump("  ", pw);
        pw.close();
    }
    mCommitted = true;
    if (mAddToBackStack) {
        mIndex = mManager.allocBackStackIndex();
    } else {
        mIndex = -1;
    }
    mManager.enqueueAction(this, allowStateLoss);
    return mIndex;
}
Copy the code

This method is the most main is invoked the FragmentManager. EnqueueAction method

3.4 FragmentManager

3.4.1 track FragmentManager overview

Once upon a time, as we know, will be record to the operation of the multiple fragments into FragmentTransaction, eventually call FragmentManager. EnqueueAction method, implement the fragments.

3.4.2 Operation call process

1. FragmentManager.enqueueAction()

void enqueueAction(@NonNull OpGenerator action, boolean allowStateLoss) {
    if (!allowStateLoss) {
        if (mHost == null) {
            if (mDestroyed) {
                throw new IllegalStateException("FragmentManager has been destroyed");
            } else {
                throw new IllegalStateException("FragmentManager has not been attached to a "
                        + "host.");
            }
        }
        checkStateLoss();
    }
    synchronized (mPendingActions) {
        if (mHost == null) {
            if (allowStateLoss) {
                // This FragmentManager isn't attached, so drop the entire transaction.
                return;
            }
            throw new IllegalStateException("Activity has been destroyed");
        }
        mPendingActions.add(action);
        scheduleCommit();
    }
}
Copy the code

This method is eventually called to the scheduleCommit() method

2. FragmentManager.scheduleCommit()

void scheduleCommit() {
    synchronized (mPendingActions) {
        boolean postponeReady =
                mPostponedTransactions != null && !mPostponedTransactions.isEmpty();
        boolean pendingReady = mPendingActions.size() == 1;
        if (postponeReady || pendingReady) {
            mHost.getHandler().removeCallbacks(mExecCommit);
            mHost.getHandler().post(mExecCommit);
            updateOnBackPressedCallbackEnabled();
        }
    }
}
Copy the code

The method ultimately executes mExecCommit through a Handler

3. FragmentManager.mExecCommit

private Runnable mExecCommit = new Runnable() { @Override public void run() { execPendingActions(true); }};Copy the code

The method ends up calling execPendingActions

4. FragmentManager.execPendingActions()

boolean execPendingActions(boolean allowStateLoss) {
    ensureExecReady(allowStateLoss);

    boolean didSomething = false;
    while (generateOpsForPendingActions(mTmpRecords, mTmpIsPop)) {
        mExecutingActions = true;
        try {
            removeRedundantOperationsAndExecute(mTmpRecords, mTmpIsPop);
        } finally {
            cleanupExec();
        }
        didSomething = true;
    }

    updateOnBackPressedCallbackEnabled();
    doPendingDeferredStart();
    mFragmentStore.burpActive();

    return didSomething;
}
Copy the code

Focus on removeRedundantOperationsAndExecute (mTmpRecords mTmpIsPop)

5. FragmentManager.removeRedundantOperationsAndExecute()

private void removeRedundantOperationsAndExecute(@NonNull ArrayList<BackStackRecord> records,
            @NonNull ArrayList<Boolean> isRecordPop) {
    
}
Copy the code

Method is long, omits the code, and finally calls executeOpsTogether

6. FragmentManager.executeOpsTogether

private void executeOpsTogether(@NonNull ArrayList<BackStackRecord> records,
          @NonNull ArrayList<Boolean> isRecordPop, int startIndex, int endIndex) {
}
Copy the code

Method is long and omits the code and eventually calls the executeOps method

7. FragmentManager.executeOps()

private static void executeOps(@NonNull ArrayList<BackStackRecord> records, @NonNull ArrayList<Boolean> isRecordPop, int startIndex, int endIndex) { for (int i = startIndex; i < endIndex; i++) { final BackStackRecord record = records.get(i); final boolean isPop = isRecordPop.get(i); if (isPop) { record.bumpBackStackNesting(-1); // Only execute the add operations at the end of // all transactions. boolean moveToState = i == (endIndex - 1); record.executePopOps(moveToState); } else { record.bumpBackStackNesting(1); record.executeOps(); }}}Copy the code

The method is divided into two kinds of circumstances, into and out of the stack, corresponding to the commit () and FragmentManager popBackStack () operation.

8. FragmentManager.executeOps()

void executeOps() { final int numOps = mOps.size(); for (int opNum = 0; opNum < numOps; opNum++) { final Op op = mOps.get(opNum); final Fragment f = op.mFragment; if (f ! = null) { f.setPopDirection(false); f.setNextTransition(mTransition); f.setSharedElementNames(mSharedElementSourceNames, mSharedElementTargetNames); } switch (op.mCmd) { case OP_ADD: f.setAnimations(op.mEnterAnim, op.mExitAnim, op.mPopEnterAnim, op.mPopExitAnim); mManager.setExitAnimationOrder(f, false); mManager.addFragment(f); break; case OP_REMOVE: f.setAnimations(op.mEnterAnim, op.mExitAnim, op.mPopEnterAnim, op.mPopExitAnim); mManager.removeFragment(f); break; case OP_HIDE: f.setAnimations(op.mEnterAnim, op.mExitAnim, op.mPopEnterAnim, op.mPopExitAnim); mManager.hideFragment(f); break; case OP_SHOW: f.setAnimations(op.mEnterAnim, op.mExitAnim, op.mPopEnterAnim, op.mPopExitAnim); mManager.setExitAnimationOrder(f, false); mManager.showFragment(f); break; case OP_DETACH: f.setAnimations(op.mEnterAnim, op.mExitAnim, op.mPopEnterAnim, op.mPopExitAnim); mManager.detachFragment(f); break; case OP_ATTACH: f.setAnimations(op.mEnterAnim, op.mExitAnim, op.mPopEnterAnim, op.mPopExitAnim); mManager.setExitAnimationOrder(f, false); mManager.attachFragment(f); break; case OP_SET_PRIMARY_NAV: mManager.setPrimaryNavigationFragment(f); break; case OP_UNSET_PRIMARY_NAV: mManager.setPrimaryNavigationFragment(null); break; case OP_SET_MAX_LIFECYCLE: mManager.setMaxLifecycle(f, op.mCurrentMaxState); break; default: throw new IllegalArgumentException("Unknown cmd: " + op.mCmd); } if (! mReorderingAllowed && op.mCmd ! = OP_ADD && f ! = null) { if (! FragmentManager.USE_STATE_MANAGER) { mManager.moveFragmentToExpectedState(f); } } } if (! mReorderingAllowed && ! FragmentManager.USE_STATE_MANAGER) { // Added fragments are added at the end to comply with prior behavior. mManager.moveToState(mManager.mCurState, true); }}Copy the code

This method does two things. It manipulates the corresponding FragmentManager method according to op. mCmd, which does not actually perform an operation and only records it, and it calls mManager.moveToState(mManager.mcurState, true).

The mManager.moveToState(Fragment F, int newState) method is eventually called, which is really the core method in the Fragment framework

Void moveToState(Fragment F, int newState)

As a core method, it was not included in Section 3.4, just to highlight its core status.

void moveToState(@NonNull Fragment f, int newState) {
    FragmentStateManager fragmentStateManager = mFragmentStore.getFragmentStateManager(f.mWho);
    if (fragmentStateManager == null) {
        // Ideally, we only call moveToState() on active Fragments. However,
        // in restoreSaveState() we can call moveToState() on retained Fragments
        // just to clean them up without them ever being added to mActive.
        // For these cases, a brand new FragmentStateManager is enough.
        fragmentStateManager = new FragmentStateManager(mLifecycleCallbacksDispatcher,
                mFragmentStore, f);
        // Only allow this FragmentStateManager to go up to CREATED at the most
        fragmentStateManager.setFragmentManagerState(Fragment.CREATED);
    }
    // When inflating an Activity view with a resource instead of using setContentView(), and
    // that resource adds a fragment using the <fragment> tag (i.e. from layout and in layout),
    // the fragment will move to the VIEW_CREATED state before the fragment manager
    // moves to CREATED. So when moving the fragment manager moves to CREATED and the
    // inflated fragment is already in VIEW_CREATED we need to move new state up from CREATED
    // to VIEW_CREATED. This avoids accidentally moving the fragment back down to CREATED
    // which would immediately destroy the Fragment's view. We rely on computeExpectedState()
    // to pull the state back down if needed.
    if (f.mFromLayout && f.mInLayout && f.mState == Fragment.VIEW_CREATED) {
        newState = Math.max(newState, Fragment.VIEW_CREATED);
    }
    newState = Math.min(newState, fragmentStateManager.computeExpectedState());
    if (f.mState <= newState) {
        // If we are moving to the same state, we do not need to give up on the animation.
        if (f.mState < newState && !mExitAnimationCancellationSignals.isEmpty()) {
            // The fragment is currently being animated...  but!  Now we
            // want to move our state back up.  Give up on waiting for the
            // animation and proceed from where we are.
            cancelExitAnimation(f);
        }
        switch (f.mState) {
            case Fragment.INITIALIZING:
                if (newState > Fragment.INITIALIZING) {
                    fragmentStateManager.attach();
                }
                // fall through
            case Fragment.ATTACHED:
                if (newState > Fragment.ATTACHED) {
                    fragmentStateManager.create();
                }
                // fall through
            case Fragment.CREATED:
                // We want to unconditionally run this anytime we do a moveToState that
                // moves the Fragment above INITIALIZING, including cases such as when
                // we move from CREATED => CREATED as part of the case fall through above.
                if (newState > Fragment.INITIALIZING) {
                    fragmentStateManager.ensureInflatedView();
                }

                if (newState > Fragment.CREATED) {
                    fragmentStateManager.createView();
                }
                // fall through
            case Fragment.VIEW_CREATED:
                if (newState > Fragment.VIEW_CREATED) {
                    fragmentStateManager.activityCreated();
                }
                // fall through
            case Fragment.ACTIVITY_CREATED:
                if (newState > Fragment.ACTIVITY_CREATED) {
                    fragmentStateManager.start();
                }
                // fall through
            case Fragment.STARTED:
                if (newState > Fragment.STARTED) {
                    fragmentStateManager.resume();
                }
        }
    } else if (f.mState > newState) {
        switch (f.mState) {
            case Fragment.RESUMED:
                if (newState < Fragment.RESUMED) {
                    fragmentStateManager.pause();
                }
                // fall through
            case Fragment.STARTED:
                if (newState < Fragment.STARTED) {
                    fragmentStateManager.stop();
                }
                // fall through
            case Fragment.ACTIVITY_CREATED:
                if (newState < Fragment.ACTIVITY_CREATED) {
                    if (isLoggingEnabled(Log.DEBUG)) {
                        Log.d(TAG, "movefrom ACTIVITY_CREATED: " + f);
                    }
                    if (f.mView != null) {
                        // Need to save the current view state if not
                        // done already.
                        if (mHost.onShouldSaveFragmentState(f) && f.mSavedViewState == null) {
                            fragmentStateManager.saveViewState();
                        }
                    }
                }
                // fall through
            case Fragment.VIEW_CREATED:
                if (newState < Fragment.VIEW_CREATED) {
                    FragmentAnim.AnimationOrAnimator anim = null;
                    if (f.mView != null && f.mContainer != null) {
                        // Stop any current animations:
                        f.mContainer.endViewTransition(f.mView);
                        f.mView.clearAnimation();
                        // If parent is being removed, no need to handle child animations.
                        if (!f.isRemovingParent()) {
                            if (mCurState > Fragment.INITIALIZING && !mDestroyed
                                    && f.mView.getVisibility() == View.VISIBLE
                                    && f.mPostponedAlpha >= 0) {
                                anim = FragmentAnim.loadAnimation(mHost.getContext(),
                                        f, false, f.getPopDirection());
                            }
                            f.mPostponedAlpha = 0;
                            // Robolectric tests do not post the animation like a real device
                            // so we should keep up with the container and view in case the
                            // fragment view is destroyed before we can remove it.
                            ViewGroup container = f.mContainer;
                            View view = f.mView;
                            if (anim != null) {
                                FragmentAnim.animateRemoveFragment(f, anim,
                                        mFragmentTransitionCallback);
                            }
                            container.removeView(view);
                            if (FragmentManager.isLoggingEnabled(Log.VERBOSE)) {
                                Log.v(FragmentManager.TAG, "Removing view " + view + " for "
                                        + "fragment " + f + " from container " + container);
                            }
                            // If the local container is different from the fragment
                            // container, that means onAnimationEnd was called, onDestroyView
                            // was dispatched and the fragment was already moved to state, so
                            // we should early return here instead of attempting to move to
                            // state again.
                            if (container != f.mContainer) {
                                return;
                            }
                        }
                    }
                    // If a fragment has an exit animation (or transition), do not destroy
                    // its view immediately and set the state after animating
                    if (mExitAnimationCancellationSignals.get(f) == null) {
                        fragmentStateManager.destroyFragmentView();
                    }
                }
                // fall through
            case Fragment.CREATED:
                if (newState < Fragment.CREATED) {
                    if (mExitAnimationCancellationSignals.get(f) != null) {
                        // We are waiting for the fragment's view to finish animating away.
                        newState = Fragment.CREATED;
                    } else {
                        fragmentStateManager.destroy();
                    }
                }
                // fall through
            case Fragment.ATTACHED:
                if (newState < Fragment.ATTACHED) {
                    fragmentStateManager.detach();
                }
        }
    }

    if (f.mState != newState) {
        if (isLoggingEnabled(Log.DEBUG)) {
            Log.d(TAG, "moveToState: Fragment state for " + f + " not updated inline; "
                    + "expected state " + newState + " found " + f.mState);
        }
        f.mState = newState;
    }
}
Copy the code

This method is the core of the entire Fragment framework, and it upgrades or degrades the Fragment’s state step by step based on the target state and the Fragment’s current state. The final callback is to the Fragment’s associated lifecycle method. At this point, the entire call chain of the COMMIT method is analyzed.

Due to the limited space, Mmanager.moveToState (Fragment F, int newState) I will write a new article, specially illustrated. LifeCycle component State changes in a similar way, step by step to upgrade or degrade.

Liver text is not easy, want to learn more dry goods knowledge, please pay attention to the “byte station” public number, if you have any questions, welcome to join the “byte station exchange group” exchange discussion, let us collision out of more thinking bar.