ViewPager2を含むViewを削除する場合、通常のViewと違う点として、ViewPager2上のFragmentが残り続けないように、Fragmentの後片付けをする必要があります。
削除する方法:ViewPager2のadapterにnullを設定する。
binding.viewPager.adapter = null
以上です。
これで終わると寂しいので、検証した内容を書いておきます。
ViewPager2はデフォルトでは表示に必要になる度にそのページを作成し、3ページ分を保持しています。TabbedActivityをViewPager2に置き換えた以下のアプリで確認してみましょう。
supportFragmentManagerから管理されているフラグメントのリストを表示させてみます。
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ツリーから削除してみましょう。
(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にして更新させれば、と思ったりしますが
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を設定することでした。
binding.viewPager.adapter = null
ViewPager2のadapterにnullを設定すると、なぜFragmentがクリアされるのか、追ってみましょう。
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で渡されます。
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()がコールされます。
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()がコールされます
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)
とコールされていき
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が呼び出されます。
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が削除されることが分かります。
@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を動的に削除することができますね。