ddog
ddog

Reputation: 670

Restoring fragments and their states in viewpager on rotation

Working on app that needs to dynamically add and remove list fragments from viewpager, when tapped on an item in the list new fragment adds to viewpager and when swipe back fragment needs to be removed, this works good but when i rotate the screen i get double instances of fragments in the pager, rotate it again and it quadruples the instances.

At the same time i need to persist state of the fragments (list position, number of items loaded, etc) this means overriding onsaveinstancestate in fragments and saving relevant data in the bundle to restore on recreation. I managed to solve the issue of double instances by clearing the adapter and calling notifydatasetchanged but then i lose saving states in fragment since onsaveinstance is not called for obvious reasons and if i don't clear the adapter it just doubles the instances. I have seen the same behavior in Dropbox app when entering and leaving folders.

This is custom pager adapter implementation i am using

 /**
 * Implementation of {@link PagerAdapter} that
 * uses a {@link Fragment} to manage each page. This class also handles
 * saving and restoring of fragment's state.
 *
 * <p>This version of the pager is more useful when there are a large number
 * of pages, working more like a list view.  When pages are not visible to
 * the user, their entire fragment may be destroyed, only keeping the saved
 * state of that fragment.  This allows the pager to hold on to much less
 * memory associated with each visited page as compared to
 * {@link FragmentPagerAdapter} at the cost of potentially more overhead when
 * switching between pages.
 *
 * <p>When using FragmentPagerAdapter the host ViewPager must have a
 * valid ID set.</p>
 *
 * <p>Subclasses only need to implement {@link #getItem(int)}
 * and {@link #getCount()} to have a working adapter. They also should
 * override {@link #getItemId(int)} if the position of the items can change.
 */
public abstract class UpdatableFragmentPagerAdapter extends PagerAdapter {

  private final FragmentManager fragmentManager;
  private final LongSparseArray<Fragment> fragmentList = new LongSparseArray<>();
  private final LongSparseArray<Fragment.SavedState> savedStatesList = new LongSparseArray<>();
  @Nullable private FragmentTransaction currentTransaction = null;
  @Nullable private Fragment currentPrimaryItem = null;

  public UpdatableFragmentPagerAdapter(@NonNull FragmentManager fm) {
    this.fragmentManager = fm;
  }

  /**
   * Return the Fragment associated with a specified position.
   */
  public abstract Fragment getItem(int position);

  @Override public void startUpdate(@NonNull ViewGroup container) {
    if (container.getId() == View.NO_ID) {
      throw new IllegalStateException("ViewPager with adapter " + this + " requires a view id");
    }
  }

  @Override @NonNull public Object instantiateItem(ViewGroup container, int position) {
    long tag = getItemId(position);
    Fragment fragment = fragmentList.get(tag);
    // If we already have this item instantiated, there is nothing
    // to do.  This can happen when we are restoring the entire pager
    // from its saved state, where the fragment manager has already
    // taken care of restoring the fragments we previously had instantiated.
    if (fragment != null) {
      return fragment;
    }

    if (currentTransaction == null) {
      currentTransaction = fragmentManager.beginTransaction();
    }

    fragment = getItem(position);
    // restore state
    final Fragment.SavedState savedState = savedStatesList.get(tag);
    if (savedState != null) {
      fragment.setInitialSavedState(savedState);
    }
    fragment.setMenuVisibility(false);
    fragment.setUserVisibleHint(false);
    fragmentList.put(tag, fragment);
    currentTransaction.add(container.getId(), fragment, "f" + tag);

    return fragment;
  }

  @Override public void destroyItem(ViewGroup container, int position, @NonNull Object object) {
    Fragment fragment = (Fragment) object;
    int currentPosition = getItemPosition(fragment);

    int index = fragmentList.indexOfValue(fragment);
    long fragmentKey = -1;
    if (index != -1) {
      fragmentKey = fragmentList.keyAt(index);
      fragmentList.removeAt(index);
    }

    //item hasn't been removed
    if (fragment.isAdded() && currentPosition != POSITION_NONE) {
      savedStatesList.put(fragmentKey, fragmentManager.saveFragmentInstanceState(fragment));
    } else {
      savedStatesList.remove(fragmentKey);
    }

    if (currentTransaction == null) {
      currentTransaction = fragmentManager.beginTransaction();
    }

    currentTransaction.remove(fragment);
  }

  @Override public void setPrimaryItem(ViewGroup container, int position, @Nullable Object object) {
    Fragment fragment = (Fragment) object;
    if (fragment != currentPrimaryItem) {
      if (currentPrimaryItem != null) {
        currentPrimaryItem.setMenuVisibility(false);
        currentPrimaryItem.setUserVisibleHint(false);
      }
      if (fragment != null) {
        fragment.setMenuVisibility(true);
        fragment.setUserVisibleHint(true);
      }
      currentPrimaryItem = fragment;
    }
  }

  @Override public void finishUpdate(ViewGroup container) {
    if (currentTransaction != null) {
      currentTransaction.commitNowAllowingStateLoss();
      currentTransaction = null;
    }
  }

  @Override public boolean isViewFromObject(@NonNull View view, @NonNull Object object) {
    return ((Fragment) object).getView() == view;
  }

  @Override public Parcelable saveState() {
    Bundle state = null;
    if (savedStatesList.size() > 0) {
      // save Fragment states
      state = new Bundle();
      long[] stateIds = new long[savedStatesList.size()];
      for (int i = 0; i < savedStatesList.size(); i++) {
        Fragment.SavedState entry = savedStatesList.valueAt(i);
        stateIds[i] = savedStatesList.keyAt(i);
        state.putParcelable(Long.toString(stateIds[i]), entry);
      }
      state.putLongArray("states", stateIds);
    }
    for (int i = 0; i < fragmentList.size(); i++) {
      Fragment f = fragmentList.valueAt(i);
      if (f != null && f.isAdded()) {
        if (state == null) {
          state = new Bundle();
        }
        String key = "f" + fragmentList.keyAt(i);
        fragmentManager.putFragment(state, key, f);
      }
    }
    return state;
  }

  @Override public void restoreState(@Nullable Parcelable state, ClassLoader loader) {
    if (state != null) {
      Bundle bundle = (Bundle) state;
      bundle.setClassLoader(loader);
      long[] fss = bundle.getLongArray("states");
      savedStatesList.clear();
      fragmentList.clear();
      if (fss != null) {
        for (long fs : fss) {
          savedStatesList.put(fs, bundle.getParcelable(Long.toString(fs)));
        }
      }
      Iterable<String> keys = bundle.keySet();
      for (String key : keys) {
        if (key.startsWith("f")) {
          Fragment f = fragmentManager.getFragment(bundle, key);
          if (f != null) {
            f.setMenuVisibility(false);
            fragmentList.put(Long.parseLong(key.substring(1)), f);
          } else {
            Timber.w("Bad fragment at key %s", key);
          }
        }
      }
    }
  }

  /**
   * Return a unique identifier for the item at the given position.
   * <p>
   * <p>The default implementation returns the given position.
   * Subclasses should override this method if the positions of items can change.</p>
   *
   * @param position Position within this adapter
   * @return Unique identifier for the item at position
   */
  public long getItemId(int position) {
    return position;
  }
}

This is the implementation of adapter

    class FolderPagerAdapter extends UpdatableFragmentPagerAdapter {

  private final FragmentManager fragmentManager;
  // Sparse array to keep track of registered fragments in memory
  private List<Fragment> addedFragments;

  FolderPagerAdapter(FragmentManager fm) {
    super(fm);
    this.fragmentManager = fm;
  }

  void init() {
    if (addedFragments == null) {
      addedFragments = new ArrayList<>();
    }
    addedFragments.clear();
    addedFragments.add(CollectionsListFragment.newInstance());
    notifyDataSetChanged();
  }

  @Override public Fragment getItem(int position) {
    return addedFragments.get(position);
  }

  @Override public long getItemId(int position) {
    return addedFragments.get(position).hashCode();
  }

  @Override public int getCount() {
    return addedFragments.size();
  }

  //this is called when notifyDataSetChanged() is called
  @Override public int getItemPosition(Object object) {
    //// refresh all fragments when data set changed
    Fragment fragment = (Fragment) object;
    if (fragment instanceof CollectionFragment) {
      return POSITION_UNCHANGED;
    } else {
      int hashCode = fragment.hashCode();
      for (int i = 0; i < addedFragments.size(); i++) {
        if (addedFragments.get(i).hashCode() == hashCode) {
          return i;
        }
      }
    }
    return PagerAdapter.POSITION_NONE;
  }

  void removeLastPage() {
    addedFragments.remove(addedFragments.size() - 1);
    notifyDataSetChanged();
  }

  void addCollectionFragment(CollectionFragment collectionFragment) {
    addedFragments.add(collectionFragment);
    notifyDataSetChanged();
  }

  void addFolderFragment(FolderFragment folderFragment) {
    addedFragments.add(folderFragment);
    notifyDataSetChanged();
  }

  void restoreFragments(List<PagerFolderCollectionModel> pagesList) {
    if (!pagesList.isEmpty()) {
      for (int i = 0; i < pagesList.size(); i++) {
        if (i == 0) {
          addedFragments.add(CollectionFragment.newInstance(pagesList.get(0).getItemId()));
        } else {
          addedFragments.add(FolderFragment.newInstance(pagesList.get(i).getItemName()));
        }
      }
      notifyDataSetChanged();
    }
  }

  void removeAll() {
    addedFragments.clear();
    notifyDataSetChanged();
  }
}

and a holder pojo which i am using to save in onsaveinstancestate in activity and restore on rotation

    public class PagerFolderCollectionModel implements Parcelable {

  public static final Parcelable.Creator<PagerFolderCollectionModel> CREATOR =
      new Parcelable.Creator<PagerFolderCollectionModel>() {
        @Override public PagerFolderCollectionModel createFromParcel(Parcel source) {
          return new PagerFolderCollectionModel(source);
        }

        @Override public PagerFolderCollectionModel[] newArray(int size) {
          return new PagerFolderCollectionModel[size];
        }
      };
  private String itemId;
  private String itemName;

  public PagerFolderCollectionModel(String itemId, String itemName) {
    this.itemId = itemId;
    this.itemName = itemName;
  }

  protected PagerFolderCollectionModel(Parcel in) {
    this.itemId = in.readString();
    this.itemName = in.readString();
  }

  public String getItemId() {
    return itemId;
  }

  public String getItemName() {
    return itemName;
  }

  @Override public int describeContents() {
    return 0;
  }

  @Override public void writeToParcel(Parcel dest, int flags) {
    dest.writeString(this.itemId);
    dest.writeString(this.itemName);
  }
}

onsaveinstance method in activity

@Override protected void onSaveInstanceState(Bundle outState) {
        super.onSaveInstanceState(outState);
        outState.putInt(STATE_SELECTED_OPTION, selectedDrawerOption);
        outState.putBoolean(STATE_SHOW_GRID_OPTION, isShowGridOption);
        outState.putParcelableArrayList(STATE_SHOWN_FRAGMENTS,
            (ArrayList<PagerFolderCollectionModel>) adapteritemslist);
        Timber.e("save");
      }

The requirement is that the first item in the adapter is always collection fragment and folder fragments are added and removed on demand (tap or swipe back)

Is there a solution for this (implementing pager adapter differently, using custom views in adapter...)? Does anybody knows how this is done in Dropbox app?

Upvotes: 2

Views: 3418

Answers (3)

ddog
ddog

Reputation: 670

I have managed to fix this by editing this code

@Override public long getItemId(int position) {
  return addedFragments.get(position).hashCode();
}

The issue was that the hashCode is generated and different on each rotation and since the requirement is not to change the position of pages i have just removed this method. It works now as expected, you can add and remove fragments with restoring state on orientation change.

Upvotes: 0

pk4393
pk4393

Reputation: 322

Can you please try to use the setRetainInstance(boolean retain) in the onCreateView method of your fragment. Set it to true. It Control whether a fragment instance is retained across Activity re-creation (such as from a configuration change).

Upvotes: 1

OneEyeQuestion
OneEyeQuestion

Reputation: 742

Have a look at this resources:

How does viewPager retain fragment states on orientation change?

Fragment in ViewPager on Fragment doesn't reload on orientation change

https://stackoverflow.com/a/27316052/2930101

Try to search for the fragments in the fragment manager first when you return them in the ViewPager.

If you manage to solve your issue I would be interested in your solution!

Upvotes: 1

Related Questions