Android

Solution for ViewPager2+Fragment Refresh Failure

Author : King of Fruits
Published Time : 2025-11-04

Preface

I recently encountered a problem when using ViewPager2 to display multiple fragments. When deleting a certain fragment, I found that the page did not delete it as expected after updating the adapter. Let's see what happened?


Problem Analysis

The commonly used method for refreshing a page is to call the notifyDataSetChanged method of the adapter. Let's first take a look at what the adapter update method of ViewPager2 does?

public class RecyclerView{

    public abstract static class Adapter<VH extends ViewHolder> {

          // ... omitting some code this time

          public final void notifyDataSetChanged() {
              mObservable.notifyChanged();
          }

    }
}

It can be seen that the update calls the RecycleView adapter method, so from here we can also intuitively know that the difference between ViewPager2 and ViewPager is the use of different adapters. ViewPager2 is implemented based on the RecycleView adapter strategy, which will not be elaborated here. Then continue to check the source code, and here are some key codes:


final LongSparseArray<Fragment> mFragments = new LongSparseArray<>();

private void ensureFragment(int position) {
    // Retrieve the ID of the current item through subscript
    long itemId = getItemId(position);
    if (!mFragments.containsKey(itemId)) {
        // TODO(133419201): check if a Fragment provided here is a new Fragment
        Fragment newFragment = createFragment(position);
        newFragment.setInitialSavedState(mSavedStates.get(itemId));
        // Save Fragment
        mFragments.put(itemId, newFragment);
    }
}

Let's take a look at this method first. From the above, it can be seen that the adapter uses the LongSparseArray variable to store fragments internally. When storing fragments, the secureFragment method is called. From the method, it can be seen that getItemId is used as the key, and creatFragment is stored as the value in mFragments. That is to say, mFragments stores fragments in the form of key value pairs, and then reads the fragments. Its key is a key. So let's take a look at how its key is generated?

public long getItemId(int position) {
    return position;
}

It can be seen that the default key is the index of the current item. So now it can be speculated that the reason for not refreshing may be because the index has not changed, so the new fragment cannot be stored, and the fragment obtained through the key, that is, the index, is still the previous fragment. To verify our hypothesis, let's move on to the next method, updateFragmentMaxLifecycle.


void updateFragmentMaxLifecycle(boolean dataSetChanged) {
     
   // ...Omit some code here
     
    // Get the index of the current item
    final int currentItem = mViewPager.getCurrentItem();
    if (currentItem >= getItemCount()) {
        /** current item is yet to be updated; it is guaranteed to change, so we will be
         * notified via {@link ViewPager2.OnPageChangeCallback#onPageSelected(int)}  */
        return;
    }
    // Get the ID of the current item, if the previous one
    long currentItemId = getItemId(currentItem);
     // If the obtained key is still the same as before, return it directly
    if (currentItemId == mPrimaryItemId && !dataSetChanged) {
        return; // nothing to do
    }
    // Retrieve the current fragment by its ID
    Fragment currentItemFragment = mFragments.get(currentItemId);
    if (currentItemFragment == null || !currentItemFragment.isAdded()) {
        return;
    }
    // Save the ID of the current item
    mPrimaryItemId = currentItemId;
    FragmentTransaction transaction = mFragmentManager.beginTransaction();

    Fragment toResume = null;
    for (int ix = 0; ix < mFragments.size(); ix++) {
        long itemId = mFragments.keyAt(ix);
        Fragment fragment = mFragments.valueAt(ix);

        if (!fragment.isAdded()) {
            continue;
        }

        if (itemId != mPrimaryItemId) {
            transaction.setMaxLifecycle(fragment, STARTED);
        } else {
            toResume = fragment; // itemId map key, so only one can match the predicate
        }

        fragment.setMenuVisibility(itemId == mPrimaryItemId);
    }
    if (toResume != null) { // in case the Fragment wasn't added yet
        transaction.setMaxLifecycle(toResume, RESUMED);
    }

    if (!transaction.isEmpty()) {
        transaction.commitNow();
    }
}

From this, it can be seen that indeed, the first step is to obtain the current index. If the current index has not changed, it will return directly. This judgment verifies why I did not refresh after deleting the first fragment before, and it returned directly. Looking at the processing of mFragments below, I believe the answer has been obtained by now. The source of all problems is this key. If we change the rules for generating keys so that they are not easily repeated like subscripts, then the problem will be solved.


problem solving

In fact, as long as the generated ItemId is not duplicated, I will directly use the hashCode value of the fragment here, and the problem will be solved.

public class SimpleChipViewPage2Adapter extends FragmentStateAdapter {

 private List<Fragment> fragments;

 public SimpleChipViewPage2Adapter(@NonNull FragmentManager fragmentManager, @NonNull Lifecycle lifecycle, List<Fragment> fragments) {
     super(fragmentManager, lifecycle);
     this.fragments = fragments;
 }

 @NonNull
 @Override
 public Fragment createFragment(int position) {
     return fragments == null ? null : fragments.get(position);
 }

 @Override
 public int getItemCount() {
     return fragments == null ? 0 : fragments.size();
 }
 // Overloading the getItemId method
 @Override
 public long getItemId(int position) {
     return fragments == null ? 0 : fragments.get(position).hashCode();
 }
}