Восстановление фрагментов и их состояний во вьюпейджере при ротации

Работая над приложением, которое должно динамически добавлять и удалять фрагменты списка из viewpager, при нажатии на элемент в списке новый фрагмент добавляется в viewpager, и когда необходимо удалить фрагмент назад, это работает хорошо, но когда я поворачиваю экран, я получаю двойной экземпляров фрагментов в пейджере, поверните его снова, и он увеличит количество экземпляров в четыре раза.

В то же время мне нужно сохранить состояние фрагментов (позиция в списке, количество загруженных элементов и т. д.), это означает переопределение onsaveinstancestate во фрагментах и ​​сохранение соответствующих данных в пакете для восстановления при воссоздании. Мне удалось решить проблему двойных экземпляров, очистив адаптер и вызвав notifydatasetchanged, но затем я теряю состояния сохранения во фрагменте, поскольку onsaveinstance не вызывается по очевидным причинам, и если я не очищаю адаптер, он просто удваивает экземпляры. Я видел такое же поведение в приложении Dropbox при входе и выходе из папок.

Это реализация пользовательского адаптера пейджера, которую я использую

 /**
 * 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;
  }
}

Это реализация адаптера

    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();
  }
}

и держатель pojo, который я использую для сохранения в onsaveinstancestate в действии и восстановления при вращении

    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 метод в действии

@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");
      }

Требование состоит в том, чтобы первым элементом в адаптере всегда был фрагмент коллекции, а фрагменты папок добавлялись и удалялись по запросу (нажмите или проведите пальцем назад).

Есть ли решение для этого (по-разному реализовать адаптер пейджера, используя пользовательские представления в адаптере...)? Кто-нибудь знает, как это делается в приложении Dropbox?


person ddog    schedule 17.01.2017    source источник


Ответы (3)


Не могли бы вы попробовать использовать setRetainInstance(логическое сохранение) в методе onCreateView вашего фрагмента. Установите значение true. Он контролирует, сохраняется ли экземпляр фрагмента при повторном создании действия (например, при изменении конфигурации).

person pk4393    schedule 17.01.2017

Взгляните на эти ресурсы:

Как viewPager сохраняет состояния фрагментов при изменении ориентации?

Фрагмент в ViewPager на фрагменте не перезагружается при изменении ориентации

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

Попробуйте сначала искать фрагменты в диспетчере фрагментов, когда вы возвращаете их в ViewPager.

Если вам удастся решить вашу проблему, я был бы заинтересован в вашем решении!

person OneEyeQuestion    schedule 17.01.2017

Мне удалось исправить это, отредактировав этот код

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

Проблема заключалась в том, что хэш-код генерируется и отличается при каждом повороте, и, поскольку требование состоит в том, чтобы не изменять положение страниц, я только что удалил этот метод. Теперь он работает как положено, вы можете добавлять и удалять фрагменты с восстановлением состояния при изменении ориентации.

person ddog    schedule 18.01.2017