1. Preface
Fragment is a very old guy. Its first submission record is in 2010, and its latest record is on June 11, 2021. It is 11 years old, but it is young and shining in Jetpack. Navigation component is a jump component based on fragment, Google's single Activity project structure is one Activity and multiple fragment project structures. Over the years, when it comes to fragment, the first impression in your mind may still stay in the development of tablet applications. It has not had so many opportunities to use fragment in mobile phone projects. On the one hand, mobile phone projects are generally implemented in a multi Activity structure, which does not involve the use of so many fragments. On the other hand, fragment is also more complex than Activity. In contrast, the use is a little troublesome, even painful, and difficult to locate. Therefore, when selecting technology, avoid it if you can. It was so difficult to use that Piwai of the Square team (the author of LeakCanary) published an article advocating against the use of fragment in 2014. In any case, after so many years, the Android team has not given up its support for fragment. Even in Jetpack, fragment began to play a more and more important role. Recently, more and more people have advocated the use of navigation. In addition, I participated in a flat board project, so I conducted a comprehensive study on the main process source code of fragment. I have gained a lot. I believe it will be of great benefit to navigation research in the future.
2. Create Fragment
According to the official documents, write a simple Demo. Simply understand the use of Fragment.
2.1 create a Fragment class
class ExampleFragment extends Fragment { public ExampleFragment() { super(R.layout.example_fragment); } } Copy code
2.2 add Fragment to Activity
Adding a Fragment to an Activity can be implemented in two ways: xml file and code programming. This article only introduces 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 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 code
We can see that fragments are mainly added to fragments through the Fragment manager_ container_ In the layout corresponding to view, the Demo is quite simple. The template code can be copied online. However, the meanings of FragmentManager, Transaction and commit() need to be further explored.
3. Deeply understand Fragment principle
3.1 what is the essence of fragment
3.1.1 the essence of fragment is View
From the previous DEMO, we can see that the Fragment will be added to the FragmentContainerView. The components that can be added to the ViewGroup must also be the View or ViewGroup. As evidenced by the source code:
//androidx.fragment.app.Fragment.java ViewGroup mContainer; // The View generated for this fragment. View mView; Copy code
In that case, why bother? Just use View instead of Fragment. addView() and removeView() are simpler and more intuitive to use.
Of course, the essence of Fragment is View, but it is not limited to this. In real projects, the interface is often very complex and the business logic is also very complex. It is often necessary 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 collaborating with Activity. In short, if developers are allowed to implement this set of logic, it will be much more troublesome than using Fragment. Although Fragment looks very complex, it is much simpler than us to implement the same function.
3.1.2 the essence of fragment is not limited to View
As a View, Fragment can be added to the layout of Activity. But it is more powerful and has the following characteristics:
- Processing life cycle is more complex than Activity life cycle
- Operate multiple fragments through FragmentTransaction
- Maintain a fallback stack through the FragmentManager and fallback 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 code
From the code, we can see that there are 8 states. It is divided into two categories: View related and Activity state related. It will be described in detail later, so far.
3.2 FragmentTransaction
3.2.1 FragmentTransaction overview
There is a course called database in software engineering. There is a concept called "transaction" in the database, which means that multiple operations are atomic, either succeed together or fail together. A typical example is transfer. Zhang San transfers 100 yuan to Li Si. There are two operations. Deduct 100 yuan from Zhang San's account and add 100 yuan to Li Si's account. With transactions, there will be no case where only one operation succeeds and another fails, otherwise the consequences will be unimaginable.
In Android, SharedPreference also has a commit method.
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 code
The above functions do three things in one submission: putString, putInt and putBoolean.
Fragment transaction is similar to SharedPreference. It can operate multiple fragments at the same time.
It is assumed that there are layout files as follows:
<LinearLayout android:orientation="vertical"> <ViewGroup1> <Fragment1 /> </ViewGroup1> <ViewGroup2> <Fragment2 /> </ViewGroup2> <ViewGroup3> <Fragment3 /> </ViewGroup3> </LinearLayout> Copy code
Suppose we want to add Fragment1 to ViewGroup1, replace it with Fragment2 on ViewGroup2, and remove the Fragment on ViewGroup3.
The pseudo code is as follows:
getSupportFragmentManager().beginTransaction() .setReorderingAllowed(true) .add(ViewGroup1, Fragment1.class, null) .replace(ViewGroup2,Fragment2.class,null) .remove(Fragment3) .commit(); Copy code
Q: why should I put these three operations into one transaction? Can't I perform these three operations in succession?
A: some scenarios are OK, some scenarios are not. When it comes to the fallback stack, pressing the return key will make a great difference whether it is placed in the transaction or not. We name the above three operations OP1, OP2 and OP3 respectively. If fragmenttransaction. Is called Addtobackstack(), when the three operations are put into the transaction, pressing the return key will perform the opposite operations to OP1, OP2 and OP3, that is, ViewGroup1, ViewGroup2 and ViewGroup3 will return to the state before the transaction. If the three operations are not put into the transaction, the state of ViewGroup3, ViewGroup2 and ViewGroup1 will be restored in turn
3.2.2 FragmentTransaction source code analysis
3.2.2.1 supported operations 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 code
You can understand it at a glance.
- OP_ADD means to add Fragment
- OP_REPLACE means to replace the Fragment on a ViewGroup
- OP_REMOVE delete Fragment
- OP_HIDE hides the Fragment, which is equivalent to view setVisibility(View.GONE)
- OP_SHOW displays Fragment, which is equivalent to view setVisibility(View.VISIBLE)
- OP_DETACH detach Fragment
- OP_ATTACH attach Fragment
The corresponding methods are:
//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 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 code
From the source code, we can see that there are ArrayList < OP > mops in the FragmentTransaction. It represents several operations that need to be processed in a transaction. The two most important fields of OP class are mCmd and mFragment. Indicates that the specified operation is performed on a Fragment.
3.2.2.3 FragmentTransaction.commit() method
//FragmentTransaction.java public abstract class FragmentTransaction { public abstract int commit(); } Copy 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 code
3.3 BackStackRecord
3.3.1 BackStackRecord overview
BackStackRecord is a subclass of FragmentTransaction, indicating that it supports simultaneous operation of multiple fragments. At the same time, as the name implies, it will be placed in the Backsack. The fallback stack is defined in the FragmentManager and is an ArrayList collection.
//FragmentManager.java ArrayList<BackStackRecord> mBackStack; Copy code
In a word, BackStackRecord supports transaction operations and will be put into the fallback stack at the same time
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 code
The main of this method is to call fragmentmanager Enqueueaction method
3.4 FragmentManager
3.4.1 FragmentManager overview
From the above, we know that operations on multiple fragments will be recorded in the FragmentTransaction, and finally the fragmentmanager will be called Enqueueaction method to really execute Fragment operation.
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 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 code
This method finally executes mExecCommit through the Handler
3. FragmentManager.mExecCommit
private Runnable mExecCommit = new Runnable() { @Override public void run() { execPendingActions(true); } }; Copy code
This method eventually calls 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 code
Focus on removedundantanoperationsandexecute (mtmprecords, mtmpispo)
5. FragmentManager.removeRedundantOperationsAndExecute()
private void removeRedundantOperationsAndExecute(@NonNull ArrayList<BackStackRecord> records, @NonNull ArrayList<Boolean> isRecordPop) { } Copy code
The method is relatively long, omitting the code, and finally calling executeOpsTogether
6. FragmentManager.executeOpsTogether
private void executeOpsTogether(@NonNull ArrayList<BackStackRecord> records, @NonNull ArrayList<Boolean> isRecordPop, int startIndex, int endIndex) { } Copy code
The method is relatively long, omitting the code, and finally calling 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 code
This method is divided into two cases: stack in and stack out, corresponding to 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 code
This method does two things: first, operate the method corresponding to FragmentManager according to OP.mCmd. This step does not really perform operations, but only record operations. Second, call mmanager moveToState(mManager.mCurState, true)
It will eventually call mmanager Movetostate (Fragment F, int newstate) method, which is the real core method in the Fragment framework
3.5 core method of fragmentmanager void moveToState(Fragment f, int newState)
As a core method, it is not placed in Chapter 3.4, just to highlight its core position.
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 code
This method is the core method in the whole Fragment framework. It will upgrade or downgrade the Fragment state step by step according to the target state and the current state of the Fragment. Finally, call back to the relevant lifecycle method of the Fragment. So far, the whole call chain of the commit method has been analyzed.
Due to limited space, mmanager Movetostate (fragment F, int newstate) I will write a new article to illustrate it. In fact, the State changes of LifeCycle components are similar, step by step upgrade or downgrade.