정상에서 IT를 외치다

프래그먼트 매니저에 대한 고찰 본문

안드로이드

프래그먼트 매니저에 대한 고찰

Black-Jin 2021. 3. 23. 21:22
반응형

1. add()

 supportFragmentManager
            .beginTransaction()
            .add(R.id.fl_container, ExampleFragment.newInstance())
            .commit()

프래그먼트를 중첩해서 화면에 보여줍니다.

 

뒤로가기 클릭 시(사용자의 완전한 활동 닫기)

단순 add 함수만으로는 스택에 쌓이지 않으므로 뒤로가기 시 부모 엑티비티가 제거되면서 모든 프래그먼트들이 onDestory 됩니다.

 

2. add() + addToBackStack()

 supportFragmentManager
            .beginTransaction()
            .add(R.id.fl_container, ExampleFragment.newInstance())
            .addToBackStack(null)
            .commit()

1번과 동일하게 화면에 프래그먼트를 중첩해서 보여줍니다. 하지만 뒤로가시 동작이 다릅니다.

 

뒤로가기 클릭 시(사용자의 완전한 활동 닫기)

addToBackStack()을 추가해주면 스택에 쌓이므로 뒤로가기 시 스택에 있는 프래그먼트들이 순서대로 onDestory 됩니다. 

 

왜 뒤로가기시 프래그먼트가 pop 될까?

뒤로가기 버튼을 누르게 되면 엑티비티의 onBackPressed 함수가 호출됩니다. 이때 ComponentActivity의 onBackPressed 의 함수를 보면 mBackPressedDispatcher의 onBackPressed 함수가 호출됨을 알 수 있습니다.

 

ComponentActivity onBackPressed 호출

    @Override
    @MainThread
    public void onBackPressed() {
        mOnBackPressedDispatcher.onBackPressed();
    }

 

OnBackPressedDispatcher

final ArrayDeque<OnBackPressedCallback> mOnBackPressedCallbacks = new ArrayDeque<>();

엑티비티가 가지고 있는 변수 onBackPressedDispatcher 는 위와 같이 OnBackPressedCallback 을 ArreyDeque로 가지고 있습니다. 

 

OnBackPressedDispacher.onBackPressed()

@MainThread
    public void onBackPressed() {
        Iterator<OnBackPressedCallback> iterator =
                mOnBackPressedCallbacks.descendingIterator();
        while (iterator.hasNext()) {
            OnBackPressedCallback callback = iterator.next();
            if (callback.isEnabled()) {
                callback.handleOnBackPressed();
                return;
            }
        }
        if (mFallbackOnBackPressed != null) {
            mFallbackOnBackPressed.run();
        }
    }

결과적으로 Activity의 뒤로가기 클릭시 onBackPressedCallback 함수를 이터레이터를 통해 모두 실행시킵니다. 우리가 프래그먼트를 생성할 때 FragmentManager의 onBackPressedCallback 변수를 위 변수에 추가해 줍니다. 이때 스택에 프래그먼트가 쌓이게 되면 FrgmentManager의 popBackStackImmediate를 호출하기 때문에 엑티비티가 종료되지 않고 스택에 쌓인 순서대로 프래그먼트가 제거됩니다.

 

addToBackStack에 대한 보충 설명

    /**
     ...
     * @param name An optional name for this back stack state, or null.
     */
    @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;
    }

FragmentTransaction 의 코드를 보겠습니다. addToBackStack의 인자로 name을 받을 수 있게 되어있습니다. 그럼 이 name의 역활은 무엇일까요? addToBackStack 을 사용해 프래그먼트를 생성하면 스택에 저장됩니다. 이경우 뒤로가기 시  FragmentManager popBackStack 을 호출하여 해당 스택의 순서대로 pop을 하게 됩니다. 이때 popBackStack에 name과 flag를 인자로 넘겨주면 해당 이름을 가진 프래그먼트를 스택의 순서와 상관없이 먼저 pop 할 수 있습니다.

 

/**
     * Pop the last fragment transition from the manager's fragment
     * back stack.
     * This function is asynchronous -- it enqueues the
     * request to pop, but the action will not be performed until the application
     * returns to its event loop.
     *
     * @param name If non-null, this is the name of a previous back state
     * to look for; if found, all states up to that state will be popped.  The
     * {@link #POP_BACK_STACK_INCLUSIVE} flag can be used to control whether
     * the named state itself is popped. If null, only the top state is popped.
     * @param flags Either 0 or {@link #POP_BACK_STACK_INCLUSIVE}.
     */
    public void popBackStack(@Nullable final String name, final int flags) {
        enqueueAction(new PopBackStackState(name, -1, flags), false);
    }

popBackStack에서 두번째 파라미터인 flag는 0과 POP_BAKC_STACK_INCLUSIVE 두개의 값을 가집니다. 0인 경우에는 해당 이름의 프래그먼트만 스택에서 먼저 onDestory 되며 POP_BACK_STACK_INCLUSIVE 를 사용할 경우 해당 이름의 프래그먼트과 그 위에 쌓여있는 프래그먼트까지 함께 onDestory 됩니다.

 

3. replace()

 supportFragmentManager
            .beginTransaction()
            .replace(R.id.fl_container, ExampleFragment.newInstance())
            .commit()

replace의 경우 기존의 생성된 프래그먼트들은 모두 onDestory 되며 새로운 프래그먼트 1개만 화면에 보여줍니다. 스택에 프래그먼트들은 쌓이지 않기 때문에 뒤로가기시 부모 Activtiy와 함께 생성된 프래그먼트 모두 onDestory 됩니다.

 

Q: addToBackStack(null) 으로 add 되어 있는 프래그먼트들이 있다면 replace 될때 어떻게 될까?

 

A라는 프래그먼트는 replace로 생성하고 B라는 프래그먼트는 add + addToBackStack(null) 으로 동작했다고 해보자.

ABBBBA 순서로 동작시키면 어떻게 될까? 마지막 A로 replace 되면서 B 프래그먼트들은 모두 onDestory 될까?

더보기

이 경우 화면에는 A 프래그먼트 1개만 존재하게 되고 4개의 B 프래그먼트는 onDestoryView가 됩니다. 여기서 뒤로가기 버튼을 누르게 되면 화면에는 A 프래그먼트가 보여지고 있지만 생성되었던 4개의 B 프래그먼트가 onDestory 됩니다. 그렇게 B 프래그먼트들이 모두 제거되어야 A 프래그먼트가 onDestory 되며 Activity가 종료됩니다.

 

Q: onDestory 되지 않은 프래그먼트를 불러올 수 있는 방법이 있을까?

val tagFragment = supportFragmentManager.findFragmentByTag("tag")

프래그먼트 생성시 tag를 추가한 경우 findFragmentByTag를 사용해 찾아올 수 있습니다.

 /**
     * Finds a fragment that was identified by the given tag either when inflated
     * from XML or as supplied when added in a transaction.  This first
     * searches through fragments that are currently added to the manager's
     * activity; if no such fragment is found, then all fragments currently
     * on the back stack are searched.
     * <p>
     * If provided a {@code null} tag, this method returns null.
     *
     * @param tag the tag used to search for the fragment
     * @return The fragment if found or null otherwise.
     */
    @Nullable
    public Fragment findFragmentByTag(@Nullable String tag) {
        return mFragmentStore.findFragmentByTag(tag);
    }

findFragmentByTag의 설명을 보면 manager에 해당 프래그먼트가 있는지 확인하고 없다면 back stack 에서 찾아본다고 합니다. 그렇다면 mFragmentStore에는 onDesotry 되지 않은 프래그먼트가 있다는 것을 우리는 알 수 있습니다. 하지만 FragmentManager 에서는 여기에 접근할 수 있는 방법을 제공해 주지 않습니다. 그 이유는 다음과 같습니다.

 

/**
     * Get a list of all fragments that are currently added to the FragmentManager.
     * This may include those that are hidden as well as those that are shown.
     * This will not include any fragments only in the back stack, or fragments that
     * are detached or removed.
     * <p>
     * The order of the fragments in the list is the order in which they were
     * added or attached.
     *
     * @return A list of all fragments that are added to the FragmentManager.
     */
    @NonNull
    @SuppressWarnings("unchecked")
    public List<Fragment> getFragments() {
        return mFragmentStore.getFragments();
    }

위 함수는 public 이므로 사용할 수 있습니다. 이 경우 FragmentManager에 추가된 프래그먼트만을 받아올 수 있습니다.

 /**
     * This is used by FragmentController to get the Active fragments.
     *
     * @return A list of active fragments in the fragment manager, including those that are in the
     * back stack.
     */
    @NonNull
    List<Fragment> getActiveFragments() {
        return mFragmentStore.getActiveFragments();
    }

이 경우 manager와 back stack에 있는 프래그먼트들 모두 불러올 수 있습니다. 하지만 public이 아니므로 우리는 사용할 수 없습니다. 오로지 findFragmentByTag를 통해 onDestory 되지는 않았지만 onDestoryView되어 있는 프래그먼트를 불러 올 수 있습니다.

 

4. replace + addToBackStack()

 supportFragmentManager
            .beginTransaction()
            .replace(R.id.fl_container, ExampleFragment.newInstance())
            .addToBackStack(null)
            .commit()

화면에 프래그먼트 1개만을 보여줍니다. 프래그먼트는 stack에 계속 쌓이지만 화면에 중첩되어 보여지지는 않습니다. 새로운 프래그먼트가 replace 될 때 이전에 생성된 프래그먼트는 onDestoryView 됩니다. 뒤로가기를 하게 되면 이전에 생성된 프래그먼트들이 순서대로 onCreateView를 실행하며 재사용됩니다.

 

사용자에 의한 종료가 아닌 프로세스에 의한 종료시 프래그먼트들은 어떻게 될까?

먼저 UI 상태 저장에 관한 구글 페이지를 보겠습니다.

시스템에서 시작된 프로세스 중단

프로세스가 중단되면서 엑티비티는 메모리에서 내려오고 saveInstanceState에 모든 프래그먼트를 저장합니다. 엑티비티가 제거되면서 모든 프래그먼트는 onDestory되고 엑티비티가 재생성되면서 saveInstanceState에서 모든 프래그먼트들을 불러와 다시 onCreate부터 시작되게 됩니다.

 

FragmentActivity의 전체 init 코드를 살펴보자

private void init() {
        // TODO: Directly connect FragmentManager to SavedStateRegistry
        getSavedStateRegistry().registerSavedStateProvider(FRAGMENTS_TAG,
                new SavedStateRegistry.SavedStateProvider() {
                    @NonNull
                    @Override
                    public Bundle saveState() {
                        Bundle outState = new Bundle();
                        markFragmentsCreated();
                        mFragmentLifecycleRegistry.handleLifecycleEvent(Lifecycle.Event.ON_STOP);
                        Parcelable p = mFragments.saveAllState();
                        if (p != null) {
                            outState.putParcelable(FRAGMENTS_TAG, p);
                        }
                        return outState;
                    }
                });
        addOnContextAvailableListener(new OnContextAvailableListener() {
            @Override
            public void onContextAvailable(@NonNull Context context) {
                mFragments.attachHost(null /*parent*/);
                Bundle savedInstanceState = getSavedStateRegistry()
                        .consumeRestoredStateForKey(FRAGMENTS_TAG);

                if (savedInstanceState != null) {
                    Parcelable p = savedInstanceState.getParcelable(FRAGMENTS_TAG);
                    mFragments.restoreSaveState(p);
                }
            }
        });
    }

코드를 정리해서 보면 아래와 같다.

getSavedStateRegistry().registerSavedStateProvider(FRAGMENTS_TAG,
                new SavedStateRegistry.SavedStateProvider() {

                //프로세스 중단시 프래그먼트의 정보를 savedState에 저장합니다.

                });
        addOnContextAvailableListener(new OnContextAvailableListener() {
            @Override
            public void onContextAvailable(@NonNull Context context) {
                Bundle savedInstanceState = getSavedStateRegistry()

                //savedInstanceState가 null이 아니면
                //저장되어 있는 프래그먼트들을 복원합니다.

            }
        });

이렇듯 프래그먼트를 사용할 때는 항상 프로세스가 중단 되었을 때를 고려하면서 작업해주어야 한다. 만일 그렇지 않다면 프래그먼트들이 중복되어 화면에 노출되는 불상사를 겪을 수 있습니다.

 

<참고자료>

google fragment lifecycle           

pluu's fragment lifecycle

fragment detail lifecycle png

반응형
Comments