Developer Android 의 Guide 문서에 따르면 Fragment 를 아래와 같이 정의하고 있습니다.
A
Fragment
represents a reusable portion of your app's UI. A fragment defines and manages its own layout, has its own lifecycle, and can handle its own input events. Fragments can't live on their own. They must be hosted by an activity or another fragment. The fragment’s view hierarchy becomes part of, or attaches to, the host’s view hierarchy.
간단히 요약하면 Fragment 는 앱 UI 의 재사용 가능한 부분을 나타내며, 자체 Layout 이 존재하며, 생명 주기가 존재하지만, 반드시 Activity 또는 다른 Fragment 에 의해 호스팅 되어야 합니다.
즉 Fragment 스스로 모든 것을 정의할 수 있지만, Activity 또는 다른 Fragment 에서 Fragment 를 Contain 할 수 있는 View(FragmentContainerView
) 에 호스팅을 해줘야 비로소 화면에 출력되며 이벤트를 받을 수 있습니다.
분석에 사용된 예제 코드는 아래와 같습니다.
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
binding = DataBindingUtil.setContentView(this, R.layout.activity_main)
if(savedInstanceState == null) {
supportFragmentManager.beginTransaction().apply {
add(R.id.fragment_container_view, MainFragment())
addToBackStack(null)
}.commit()
}
}
1. Fragment 를 add, replace, remove 등의 책임을 지닌 FragmentManager 인스턴스를 가져옵니다.
FragmentActivity
의 getSupportFragmentManager()
→ FragmentController
의 getSupportFragmentManager()
→ FragmentController
가 소유한 FragmentHostCallback
의 mFragmentManager
필드에 저장된 FragmentManager
인스턴스를 가져옵니다.
FragmentController
는 Fragment Host 에 FragmentManager
와의 integration point 를 제공합니다.
FragmentActivity
→ FragmentController
→ FragmentHostCallback
→ FragmentManager
방향의 관계를 형성합니다.
public class FragmentActivity extends ComponentActivity implements
ActivityCompat.OnRequestPermissionsResultCallback,
ActivityCompat.RequestPermissionsRequestCodeValidator {
final FragmentController mFragments = FragmentController.createController(new HostCallbacks());
@NonNull
public FragmentManager getSupportFragmentManager() {
return mFragments.getSupportFragmentManager();
}
class HostCallbacks extends FragmentHostCallback<FragmentActivity> implements
OnConfigurationChangedProvider,
OnTrimMemoryProvider,
OnMultiWindowModeChangedProvider,
OnPictureInPictureModeChangedProvider,
ViewModelStoreOwner,
OnBackPressedDispatcherOwner,
ActivityResultRegistryOwner,
SavedStateRegistryOwner,
FragmentOnAttachListener,
MenuHost {
public HostCallbacks() {
super(FragmentActivity.this /*fragmentActivity*/);
}
}
}
public class FragmentController {
private final FragmentHostCallback<?> mHost;
@NonNull
public static FragmentController createController(@NonNull FragmentHostCallback<?> callbacks) {
return new FragmentController(checkNotNull(callbacks, "callbacks == null"));
}
private FragmentController(FragmentHostCallback<?> callbacks) {
mHost = callbacks;
}
@NonNull
public FragmentManager getSupportFragmentManager() {
return mHost.mFragmentManager;
}
}
public abstract class FragmentHostCallback<E> extends FragmentContainer {
final FragmentManager mFragmentManager = new FragmentManagerImpl();
}
2. FragmentManager 의 beginTransaction() 메서드를 호출하여 FragmentTransaction 타입의 인스턴스를 생성합니다.
FragmentManager
의 beginTransaction()
메서드 내에서는 FragmentTransaction
을 상속받은 BackStackRecord
인스턴스를 생성하여 반환합니다.
BackStackRecord
는 Fragment 관련 명령어들을 담고, 담겨진 명령어들을 commit 하여 담겨진 명령어가 실행될 수 있도록 하는 역할을 수행합니다.
public abstract class FragmentManager implements FragmentResultOwner {
@NonNull
public FragmentTransaction beginTransaction() {
return new BackStackRecord(this);
}
}
final class BackStackRecord extends FragmentTransaction implements
FragmentManager.BackStackEntry, FragmentManager.OpGenerator {
final FragmentManager mManager;
FragmentTransaction(@NonNull FragmentFactory fragmentFactory, @Nullable ClassLoader classLoader) {
mFragmentFactory = fragmentFactory;
mClassLoader = classLoader;
}
BackStackRecord(@NonNull FragmentManager manager) {
super(manager.getFragmentFactory(), manager.getHost() != null
? manager.getHost().getContext().getClassLoader()
: null);
mManager = manager;
}
}
3. 생성된 FragmentTransaction 인스턴스에 add 명령어를 담습니다.
FragmentTransaction
의 add()
→ BackStackRecord
의 doAddOp()
→ FragmentTransaction
의 doAddOp()
순서로 메서드가 호출됩니다.
FragmentTransaction
의 doAddOp()
메서드는 Fragment 클래스가 익명 클래스 이거나 또는 public 클래스가 아닌 경우, 또는 멤버 클래스 이고 static 클래스가 아닌 경우 예외를 발생시킵니다.(즉 Fragment 는 익명 클래스가 아니어야 하고, public 클래스여야 하며, 멤버 클래스가 아니어야 하고, static 클래스여야 합니다.) 그 이후에 FragmentContainerView
의 ID 가 할당된 경우 Fragment 의 mContainerId 와 mFragmentId 를 FragmentContainerView
ID 로 설정합니다.
FragmentTransaction
의 static 멤버 클래스인 Op
의 인스턴스를 생성합니다. 생성 시에 수행할 명령어와 명령어의 대상인 Fragment 등의 정보를 Op
에 설정합니다.
addOp()
메서드를 호출하여 FragmentTransaction
의 mOps 필드에 생성한 Op
인스턴스를 저장합니다.
BackStackRecord
의 doAddOp()
메서드로 돌아와 Fragment 의 mFragmentManager 필드를 Fragment 를 호스팅하는 현재 Activity 와 연결된 FragmentManager
인스턴스로 설정합니다. Fragment 에서 getParentFragmentManager()
와 같은 메서드를 호출할 때 반환되는 FragmentManager
입니다.
public abstract class FragmentTransaction {
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;
static final class Op {
int mCmd;
Fragment mFragment;
boolean mFromExpandedOp;
int mEnterAnim;
int mExitAnim;
int mPopEnterAnim;
int mPopExitAnim;
Lifecycle.State mOldMaxState;
Lifecycle.State mCurrentMaxState;
Op(int cmd, Fragment fragment) {
this.mCmd = cmd;
this.mFragment = fragment;
this.mFromExpandedOp = false;
this.mOldMaxState = Lifecycle.State.RESUMED;
this.mCurrentMaxState = Lifecycle.State.RESUMED;
}
}
ArrayList<Op> mOps = new ArrayList<>();
@NonNull
public FragmentTransaction add(@IdRes int containerViewId, @NonNull Fragment fragment) {
doAddOp(containerViewId, fragment, null, OP_ADD);
return this;
}
void doAddOp(int containerViewId, Fragment fragment, @Nullable String tag, int opcmd) {
if (fragment.mPreviousWho != null) {
FragmentStrictMode.onFragmentReuse(fragment, fragment.mPreviousWho);
}
final Class<?> fragmentClass = fragment.getClass();
final int modifiers = fragmentClass.getModifiers();
if (fragmentClass.isAnonymousClass() || !Modifier.isPublic(modifiers)
|| (fragmentClass.isMemberClass() && !Modifier.isStatic(modifiers))) {
throw new IllegalStateException("Fragment " + fragmentClass.getCanonicalName()
+ " must be a public static class to be properly recreated from"
+ " instance state.");
}
if (tag != null) {
if (fragment.mTag != null && !tag.equals(fragment.mTag)) {
throw new IllegalStateException("Can't change tag of fragment "
+ fragment + ": was " + fragment.mTag
+ " now " + tag);
}
fragment.mTag = tag;
}
if (containerViewId != 0) {
if (containerViewId == View.NO_ID) {
throw new IllegalArgumentException("Can't add fragment "
+ fragment + " with tag " + tag + " to container view with no id");
}
if (fragment.mFragmentId != 0 && fragment.mFragmentId != containerViewId) {
throw new IllegalStateException("Can't change container ID of fragment "
+ fragment + ": was " + fragment.mFragmentId
+ " now " + containerViewId);
}
fragment.mContainerId = fragment.mFragmentId = containerViewId;
}
addOp(new Op(opcmd, fragment));
}
void addOp(Op op) {
mOps.add(op);
op.mEnterAnim = mEnterAnim;
op.mExitAnim = mExitAnim;
op.mPopEnterAnim = mPopEnterAnim;
op.mPopExitAnim = mPopExitAnim;
}
}
final class BackStackRecord extends FragmentTransaction implements
FragmentManager.BackStackEntry, FragmentManager.OpGenerator {
@Override
void doAddOp(int containerViewId, Fragment fragment, @Nullable String tag, int opcmd) {
super.doAddOp(containerViewId, fragment, tag, opcmd);
fragment.mFragmentManager = mManager;
}
}
4. addToBackStack() 메서드를 호출합니다.
FragmentTransaction
의 mAllowAddToBackStack flag 가 false 인 경우 현재 FragmentTransaction
은 BackStack 에 추가될 수 없다는 예외를 발생시킵니다. mAllowAddToBackStack flag 는 disallowAddToBackStack()
메서드를 통해서 false 로 설정할 수 있습니다.
mAddToBackStack flag 를 true 로 설정합니다.
public abstract class FragmentTransaction {
boolean mAllowAddToBackStack = true;
boolean mAddToBackStack;
@Nullable String mName;
@NonNull
public FragmentTransaction addToBackStack(@Nullable String name) {
if (!mAllowAddToBackStack) {
throw new IllegalStateException(
"This FragmentTransaction is not allowed to be added to the back stack.");
}
mAddToBackStack = true;
mName = name;
return this;
}
@NonNull
public FragmentTransaction disallowAddToBackStack() {
if (mAddToBackStack) {
throw new IllegalStateException(
"This transaction is already being added to the back stack");
}
mAllowAddToBackStack = false;
return this;
}
}
5. commit() 메서드를 호출하여 FragmentTransaction 에 모든 명령어들이 담겼으니 FragmentManager 를 통해 명령어를 실행하라 알립니다.
BackStackRecord
의 commitInternal()
메서드에서 mCommitted flag 를 검사하여 true 인 경우 이미 현재 FragmentTransaction 이 commit 되었다고 판단하여 예외를 발생시킵니다.
mAddToBackStack flag 가 true 인 경우 beginTransaction()
메서드 내에서 BackStackRecord
를 생성할 때 전달한 FragmentManager
의 allocBackStackIndex()
메서드를 호출하여 FragmentManager
의 mBackStackIndex 현재 값을 반환하고 1 증가시킵니다.
FragmentManager
의 enqueueAction()
메서드를 호출합니다.
final class BackStackRecord extends FragmentTransaction implements
FragmentManager.BackStackEntry, FragmentManager.OpGenerator {
boolean mCommitted;
@Override
public int commit() {
return commitInternal(false);
}
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;
}
}
public abstract class FragmentManager implements FragmentResultOwner {
private final AtomicInteger mBackStackIndex = new AtomicInteger();
int allocBackStackIndex() {
return mBackStackIndex.getAndIncrement();
}
}
6. FragmentManager 의 enqueueAction() 메서드가 호출됩니다.
FragmentManager
의 enqueueAction()
메서드 내에서 mPendingActions 에 매개변수로 전달받은 OpGenerator
인터페이스의 구현 클래스인 BackStackRecord
를 저장 후, scheduleCommit()
메서드를 호출합니다.
FragmentHostCallback
의 getHandler()
를 통해 얻은 Handler
에 mExecCommit Runnable
을 전달합니다.
Runnable
의 run()
메서드에서 execPendingActions()
메서드를 호출합니다.
execPendingActions()
메서드 내에서 generateOpsForPendingActions()
메서드를 호출하여 BackStackRecord
의 generateOps()
메서드를 호출합니다.
generateOps()
내에서 Host 인 Activity 와 연결된 FragmentManager
의 addBackStackState()
메서드를 호출하여 BackStack 에 해당 FragmentTransaction
을 저장합니다.
generateOpsForPendingActions()
메서드가 종료된 후 removeRedundantOperationsAndExecute()
메서드를 호출합니다.
removeRedundantOperationsAndExecute()
메서드 내에서 executeOps()
메서드를 호출합니다.
FragmentManager
의 executeOps()
메서드 내에서 매개변수로 전달받은 BackStackRecord
List 를 순회하며 BackStackRecord
의 executeOps()
메서드를 호출합니다.
BackStackRecord
의 executeOps()
메서드에서 mOps 에 저장된 명령어들을 하나씩 순회하며 각 명령어에 맞는 기능을 실행합니다. 예를들어 꺼내온 명령어가 OP_ADD 명령어인 경우 FragmentManager
의 addFragment()
를 실행합니다.
간단히 요약하면 commit() 메서드가 실행되면 각 명령어들이 저장된 FragmentTransaction 을 FragmentManager 가 소유한 BackStack 에 저장하고, FragmentTransaction 에 저장된 각 명령어들을 꺼내서 명령어에 맞는 FragmentManager 메서드를 호출하는 방식으로 동작합니다.
public abstract class FragmentManager implements FragmentResultOwner {
ArrayList<BackStackRecord> mBackStack;
private final ArrayList<OpGenerator> mPendingActions = new ArrayList<>();
private Runnable mExecCommit = new Runnable() {
@Override
public void run() {
execPendingActions(true);
}
};
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();
}
}
void scheduleCommit() {
synchronized (mPendingActions) {
boolean pendingReady = mPendingActions.size() == 1;
if (pendingReady) {
mHost.getHandler().removeCallbacks(mExecCommit);
mHost.getHandler().post(mExecCommit);
updateOnBackPressedCallbackEnabled();
}
}
}
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;
}
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);
record.executePopOps();
} else {
record.bumpBackStackNesting(1);
record.executeOps();
}
}
}
void addBackStackState(BackStackRecord state) {
if (mBackStack == null) {
mBackStack = new ArrayList<>();
}
mBackStack.add(state);
}
}
final class BackStackRecord extends FragmentTransaction implements
FragmentManager.BackStackEntry, FragmentManager.OpGenerator {
@Override
public boolean generateOps(@NonNull ArrayList<BackStackRecord> records,
@NonNull ArrayList<Boolean> isRecordPop) {
if (FragmentManager.isLoggingEnabled(Log.VERBOSE)) {
Log.v(TAG, "Run: " + this);
}
records.add(this);
isRecordPop.add(false);
if (mAddToBackStack) {
mManager.addBackStackState(this);
}
return true;
}
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.mBeingSaved = mBeingSaved;
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);
}
}
}
}