0
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 1 year has passed since last update.

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

Last updated at Posted at 2023-03-13

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がクリアされるのか、追ってみましょう。

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を動的に削除することができますね。

0
1
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?