ViewPager2を動的にViewツリーから削除する

ViewPager2を含むViewを削除する場合、通常のViewと違う点として、ViewPager2上のFragmentが残り続けないように、Fragmentの後片付けをする必要があります。

削除する方法：ViewPager2のadapterにnullを設定する。

```kotlin
binding.viewPager.adapter = null
```

以上です。

---

これで終わると寂しいので、検証した内容を書いておきます。

ViewPager2はデフォルトでは表示に必要になる度にそのページを作成し、3ページ分を保持しています。TabbedActivityをViewPager2に置き換えた以下のアプリで確認してみましょう。

<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/155171/b3fd033b-7570-07c7-128f-7d3a01d37b86.png" width="300" />

supportFragmentManagerから管理されているフラグメントのリストを表示させてみます。

```kotlin
supportFragmentManager.fragments.forEach {
    Log.e("XXXX", "fragments: $it")
}
```

3つのタブをすべて表示した後では以下のように3つのFragmentが管理されていることが分かります。

```
fragments: PlaceholderFragment{953350} (b81c2a0d-0bf4-4db5-9a30-3b8bd9c084d0 tag=f0)
fragments: PlaceholderFragment{5096949} (2785584c-fc8e-4c8f-960e-39e9ccb20668 tag=f1)
fragments: PlaceholderFragment{a8c424e} (59dd12ef-b063-4a0a-92d9-3054f6d16445 tag=f2)
```

では、ViewPager2をこのActivityのViewツリーから削除してみましょう。

```kotlin
(binding.viewPager.parent as? ViewGroup)?.removeView(binding.viewPager)
```

ViewPager2はViewツリーから削除されていますので、その上にあるFragmentも同様に削除されているはずですが、同様にfragmentのリストを表示させてみると、以下のように3つとも保持されたままになっています。

```
fragments: PlaceholderFragment{953350} (b81c2a0d-0bf4-4db5-9a30-3b8bd9c084d0 tag=f0)
fragments: PlaceholderFragment{5096949} (2785584c-fc8e-4c8f-960e-39e9ccb20668 tag=f1)
fragments: PlaceholderFragment{a8c424e} (59dd12ef-b063-4a0a-92d9-3054f6d16445 tag=f2)
```

例えば、ある目的で、一時的にViewPager2を作成・表示し、その処理が終わったらViewPager2自体を削除する。ということをしようとすると、ViewPager2を追加する度にFragmentが増えていってしまいます。これはよろしくない。

しかし、ViewPager2のFragmentはFragmentStateAdapterの内部で管理されており、継承したクラスからはアクセスができません。また、例えば以下のように、itemCountを0にして更新させれば、と思ったりしますが

```kotlin
private var cleared = false

fun clear() {
    cleared = true
    notifyDataSetChanged()
}

override fun getItemCount(): Int = if (cleared) 0 else 3
```

これだと、削除されるのは表示されているFragmentのみのようです。キャッシュまではクリアされないのですね。

```
fragments: PlaceholderFragment{44f26f2} (5a139925-42e8-4eba-9b70-ce41b5746c6e tag=f1)
fragments: PlaceholderFragment{4bd8143} (a782e573-a539-4f8c-88c0-675a1ca32eed tag=f2)
```

これを解決する一番簡単な方法はadapterにnullを設定することでした。

```kotlin
binding.viewPager.adapter = null
```

ViewPager2のadapterにnullを設定すると、なぜFragmentがクリアされるのか、追ってみましょう。

```ViewPager2.java
public void setAdapter(@Nullable @SuppressWarnings("rawtypes") Adapter adapter) {
    final Adapter<?> currentAdapter = mRecyclerView.getAdapter();
    mAccessibilityProvider.onDetachAdapter(currentAdapter);
    unregisterCurrentItemDataSetTracker(currentAdapter);
    mRecyclerView.setAdapter(adapter);
    mCurrentItem = 0;
    restorePendingState();
    mAccessibilityProvider.onAttachAdapter(adapter);
    registerCurrentItemDataSetTracker(adapter);
}
```

ViewPager2のAdapterはRecyclerViewのAdapterであり、内部的にはRecyclerViewが使われています。
ViewPager2の内部で保持されているRecyclerViewに対して、setAdapterで渡されます。

```RecyclerView.java
public void setAdapter(@Nullable Adapter adapter) {
    // bail out if layout is frozen
    setLayoutFrozen(false);
    setAdapterInternal(adapter, false, true);
    processDataSetCompletelyChanged(false);
    requestLayout();
}
```

setAdapterInternalの第二引数compatibleWithPreviousがfalse、第三引数removeAndRecycleViewsがtrueで呼び出されます。
前のAdapterとViewHolderに互換性がない、かつ、既存のViewをすべて削除せよ、ということです。
removeAndRecycleViews()がコールされます。

```RecyclerView.java
private void setAdapterInternal(@Nullable Adapter adapter, boolean compatibleWithPrevious,
        boolean removeAndRecycleViews) {
    if (mAdapter != null) {
        mAdapter.unregisterAdapterDataObserver(mObserver);
        mAdapter.onDetachedFromRecyclerView(this);
    }
    if (!compatibleWithPrevious || removeAndRecycleViews) {
        removeAndRecycleViews();
    }
    mAdapterHelper.reset();
    final Adapter oldAdapter = mAdapter;
    mAdapter = adapter;
    if (adapter != null) {
        adapter.registerAdapterDataObserver(mObserver);
        adapter.onAttachedToRecyclerView(this);
    }
    if (mLayout != null) {
        mLayout.onAdapterChanged(oldAdapter, mAdapter);
    }
    mRecycler.onAdapterChanged(oldAdapter, mAdapter, compatibleWithPrevious);
    mState.mStructureChanged = true;
}
```

mRecycler.clear()がコールされます

```RecyclerView.java
void removeAndRecycleViews() {
    if (mItemAnimator != null) {
        mItemAnimator.endAnimations();
    }
    if (mLayout != null) {
        mLayout.removeAndRecycleAllViews(mRecycler);
        mLayout.removeAndRecycleScrapInt(mRecycler);
    }
    mRecycler.clear();
}
```

recycleAndClearCachedViews()
recycleCachedViewAt(i)
addViewHolderToRecycledViewPool(viewHolder, true)
dispatchViewRecycled(holder)
とコールされていき

```RecyclerView.java
public void clear() {
    mAttachedScrap.clear();
    recycleAndClearCachedViews();
}

void recycleAndClearCachedViews() {
    final int count = mCachedViews.size();
    for (int i = count - 1; i >= 0; i--) {
        recycleCachedViewAt(i);
    }
    mCachedViews.clear();
    if (ALLOW_THREAD_GAP_WORK) {
        mPrefetchRegistry.clearPrefetchPositions();
    }
}

void recycleCachedViewAt(int cachedViewIndex) {
    if (DEBUG) {
        Log.d(TAG, "Recycling cached view at index " + cachedViewIndex);
    }
    ViewHolder viewHolder = mCachedViews.get(cachedViewIndex);
    if (DEBUG) {
        Log.d(TAG, "CachedViewHolder to be recycled: " + viewHolder);
    }
    addViewHolderToRecycledViewPool(viewHolder, true);
    mCachedViews.remove(cachedViewIndex);
}

void addViewHolderToRecycledViewPool(@NonNull ViewHolder holder, boolean dispatchRecycled) {
    clearNestedRecyclerViewIfNotNested(holder);
    View itemView = holder.itemView;
    if (mAccessibilityDelegate != null) {
        AccessibilityDelegateCompat itemDelegate = mAccessibilityDelegate.getItemDelegate();
        AccessibilityDelegateCompat originalDelegate = null;
        if (itemDelegate instanceof RecyclerViewAccessibilityDelegate.ItemDelegate) {
            originalDelegate =
                    ((RecyclerViewAccessibilityDelegate.ItemDelegate) itemDelegate)
                            .getAndRemoveOriginalDelegateForItem(itemView);
        }
        // Set the a11y delegate back to whatever the original delegate was.
        ViewCompat.setAccessibilityDelegate(itemView, originalDelegate);
    }
    if (dispatchRecycled) {
        dispatchViewRecycled(holder);
    }
    holder.mOwnerRecyclerView = null;
    getRecycledViewPool().putRecycledView(holder);
}
```

dispatchViewRecycled()の中で、mAdapter.onViewRecycled(holder)がコールされ、Adapterが呼び出されます。

```RecyclerView.java
void dispatchViewRecycled(@NonNull ViewHolder holder) {
    if (mRecyclerListener != null) {
        mRecyclerListener.onViewRecycled(holder);
    }
    if (mAdapter != null) {
        mAdapter.onViewRecycled(holder);
    }
    if (mState != null) {
        mViewInfoStore.removeViewHolder(holder);
    }
    if (DEBUG) Log.d(TAG, "dispatchViewRecycled: " + holder);
}
```

FragmentStateAdapterのonViewRecycledの実装は以下のようになっており、ここでViewHolderに紐付いたFragmentが削除されています。
すべての保持されているViewHolderについてforループで呼び出されていましたので、これですべてのFragmentが削除されることが分かります。

```FragmentStateAdapter.java
@Override
public final void onViewRecycled(@NonNull FragmentViewHolder holder) {
    final int viewHolderId = holder.getContainer().getId();
    final Long boundItemId = itemForViewHolder(viewHolderId); // item currently bound to the VH
    if (boundItemId != null) {
        removeFragment(boundItemId);
        mItemIdToViewHolder.remove(boundItemId);
    }
}

private void removeFragment(long itemId) {
    Fragment fragment = mFragments.get(itemId);

    if (fragment == null) {
        return;
    }

    if (fragment.getView() != null) {
        ViewParent viewParent = fragment.getView().getParent();
        if (viewParent != null) {
            ((FrameLayout) viewParent).removeAllViews();
        }
    }

    if (!containsItem(itemId)) {
        mSavedStates.remove(itemId);
    }

    if (!fragment.isAdded()) {
        mFragments.remove(itemId);
        return;
    }

    if (shouldDelayFragmentTransactions()) {
        mHasStaleFragments = true;
        return;
    }

    if (fragment.isAdded() && containsItem(itemId)) {
        mSavedStates.put(itemId, mFragmentManager.saveFragmentInstanceState(fragment));
    }
    mFragmentManager.beginTransaction().remove(fragment).commitNow();
    mFragments.remove(itemId);
}
```

commitNow()で削除されていて、途中ディスパッティ処理も挟まっていないので、setAdapter(null)の処理を抜けた時点ですべてのFragmentが削除された状態になっています。
これで安心してViewPager2を動的に削除することができますね。