Ruuhkis
Ruuhkis

Reputation: 1934

Android changing fragment order inside FragmentPagerAdapter

I have ~20 fragments inside FragmentPagerAdapter that contains a list where it picks the fragments from so theres no recreation of fragments.

private List<TitledFragment> fragments;

public SectionsPagerAdapter(FragmentManager fm) {
    super(fm);

    this.fragments = new ArrayList<TitledFragment>();
}

@Override
public Fragment getItem(int i) {
    return fragments.get(i).getFragment();
}

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

@Override
public CharSequence getPageTitle(int position) {
   return fragments.get(position).getTitle();
}

public synchronized List<TitledFragment> getFragments() {
    return fragments;
}


public synchronized void addFragment(TitledFragment fragment) {
    fragments.add(fragment);
    notifyDataSetChanged();
}

the FragmentPagerAdapter is set as adapter of ViewPager and after that I reorder the fragments(shuffling was just for test)

Collections.shuffle(mSectionsPagerAdapter.getFragments());
mSectionsPagerAdapter.notifyDataSetChanged();

and for some reason only order of titles is being changed, even if its returning different fragment for getItem as the order is different. Why is this and how can I get around it?

Upvotes: 2

Views: 5243

Answers (2)

BitByteDog
BitByteDog

Reputation: 3484

The accepted answer is a non-optimal answer. Returning POSITION_NONE from int getItemPosition(Object) just destroys any hope of efficient fragment management, requiring all fragnents to be reinstantiated. It also ignores another problem. The FragmentPageAdapter keeps a cached copy of the Fragment in the FragmentManager, and it looks for those copies when instantiating a new Fragment. If it finds what it thinks is a matching fragment, the public Fragment getItem(int) method is not called and the cached copy is used.

For example, lets say pages 0 and 1 are loaded, there will be cached fragments tagged with 0 and 1 in the FragmentManager. Now a page is inserted at index 0 (don't forget to call notifyDataSetChanged()), the old index 0 becomes 1 and 1 becomes 2 (this is signalled using the method public int FragmentPageAdapter.getItemPosition(Object)). For item 0 POSITION_NONE was returned (as it is the new position), so the method public Object instantiateItem(ViewGroup, int) is called for position 0:

public Object instantiateItem(ViewGroup container, int position) {
    if (mCurTransaction == null) {
        mCurTransaction = mFragmentManager.beginTransaction();
    }

    final long itemId = getItemId(position);

    // Do we already have this fragment?
    String name = makeFragmentName(container.getId(), itemId);
    Fragment fragment = mFragmentManager.findFragmentByTag(name);
    if (fragment != null) {
        if (DEBUG) Log.v(TAG, "Attaching item #" + itemId + ": f=" + fragment);
        mCurTransaction.attach(fragment);
    } else {
        fragment = getItem(position);
        ...

See what happens, a cached Fragment is found for position 0, and the fragment you wanted in position 1 is now in position 0, the fragment you wanted in position 2 is now in position 1 and in position 2 you get a new fragment returned by FragmentPageAdapter.getItem(int) that is a duplicate of position 1.

How to solve this? I have seen many suggestions on SO including:

  1. Always return POSITION_NONE from FragmentPageAdapter.getItemPosition() https://stackoverflow.com/a/7386616/2351246 - this answer ignores efficiciency and memory management (and will eventually not work without clearing out cached fragments)
  2. Tracking the fragment tags https://stackoverflow.com/a/12104399/2351246, this relies on implementation details that could change.
  3. And worst of all use magic by reverse engineering the FragmentPageAdapter implementation https://stackoverflow.com/a/13925130/2351246, this is just horrible.

There is no need to ruin the memory management or to track the internal implementation details of FragmentPagerAdapter. The missing detail in all the answers is that a FragmentPagerAdapter that wants to reorder fragments must implement the method public long getItemId(int position) also:

    @Override
    public long getItemId(int position) {
        return System.identityHashCode(fragments.get(position));
    }

This provides a non position based ID that can be used to look up the correct fragment in the FragmentManager cache even if it moves page.

Upvotes: 1

UgglyNoodle
UgglyNoodle

Reputation: 3047

You also need to override getItemPosition(). By default, this function always returns POSITION_UNCHANGED, so that when you call notifyDataSetChanged(), the adapter assumes that all of the fragments are still in the same position. (It does not call getItem() again, since it thinks all of the necessary fragments are already created.) Your new getItemPosition() should return the new position of each fragment after shuffling.

An easy hack is to always return POSITION_NONE, and then the adapter will just discard all of the existing fragments, and recreate them in their correct positions when calling getItem(). Of course, this is not efficient, and has the additional drawback that the current fragment being viewed may change. However, since there may possibly be some bugs in reordering the fragments - see my question here - the POSITION_NONE approach may be the safest method.

Upvotes: 9

Related Questions