32
votes

I've got this activity, which holds a fragment. This fragment layout consists of a view pager with several fragments (two, actually).

When the view pager is created, its adapter is created, getItem gets called and my sub fragments are created. Great.

Now when I rotate the screen, the framework handles the fragment re-creation, the adapter is created again in my onCreate from the main fragment, but getItem never gets called, so my adapter holds wrong references (actually nulls) instead of the two fragments.

What I have found is that the fragment manager (that is, the child fragment manager) contains an array of fragments called mActive, which is of course not accessible from code. However there's this getFragment method:

@Override
public Fragment getFragment(Bundle bundle, String key) {
    int index = bundle.getInt(key, -1);
    if (index == -1) {
        return null;
    }
    if (index >= mActive.size()) {
        throwException(new IllegalStateException("Fragement no longer exists for key "
                + key + ": index " + index));
    }
    Fragment f = mActive.get(index);
    if (f == null) {
        throwException(new IllegalStateException("Fragement no longer exists for key "
                + key + ": index " + index));
    }
    return f;
}

I won't comment the typo :)
This is the hack I have implemented in order to update the references to my fragments, in my adapter constructor:

// fm holds a reference to a FragmentManager
Bundle hack = new Bundle();
try {
    for (int i = 0; i < mFragments.length; i++) {
        hack.putInt("hack", i);
        mFragments[i] = fm.getFragment(hack, "hack");
    }
} catch (Exception e) {
    // No need to fail here, likely because it's the first creation and mActive is empty
}

I am not proud. This works, but it's ugly. What's the actual way of having a valid adapter after a screen rotation?

PS: here's the full code

6
same issue here : stackoverflow.com/questions/16299626/… Have you find something?Poutrathor
Nope, still using my hack. I had already checkd the issue you're mentioning but this didn't solve my problem.Benoit Duffez
I am sorry but I am still learning Android and I cannot understand your hack. Can you explain it a bit more for me please? You put that code in your constructor, the fm is the FragmentManager given as argument, but what is mFragment? Why do you put all int to 0 ? thanksPoutrathor
You're right there's a mistake that I forgot to fix here (it's fixed on my code). I will edit my answer and publish all the code.Benoit Duffez

6 Answers

29
votes

I had the same issue - I assume you're subclassing FragmentPagerAdapter for your pager adapter (as getItem() is specific to FragmentPagerAdapter).

My solution was to instead subclass PagerAdapter and handle the fragment creation/deletion yourself (reimplementing some of the FragmentPagerAdapter code):

public class ListPagerAdapter extends PagerAdapter {
    FragmentManager fragmentManager;
    Fragment[] fragments;

    public ListPagerAdapter(FragmentManager fm){
        fragmentManager = fm;
        fragments = new Fragment[5];
    }

    @Override
    public void destroyItem(ViewGroup container, int position, Object object) {
        assert(0 <= position && position < fragments.length);
        FragmentTransaction trans = fragmentManager.beginTransaction();
        trans.remove(fragments[position]);
        trans.commit();
        fragments[position] = null;
}

    @Override
    public Fragment instantiateItem(ViewGroup container, int position){
        Fragment fragment = getItem(position);
        FragmentTransaction trans = fragmentManager.beginTransaction();
        trans.add(container.getId(),fragment,"fragment:"+position);
        trans.commit();
        return fragment;
    }

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

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

    public Fragment getItem(int position){
        assert(0 <= position && position < fragments.length);
        if(fragments[position] == null){
            fragments[position] = ; //make your fragment here
        }
        return fragments[position];
    }
}

Hope this helps.

7
votes

My answer is kind of similar to Joshua Hunt's one, but by commiting the transaction in finishUpdate method you get much better performance. One transaction instead of two per update. Here is the code:

private class SuchPagerAdapter extends PagerAdapter{

    private final FragmentManager mFragmentManager;
    private SparseArray<Fragment> mFragments;
    private FragmentTransaction mCurTransaction;

    private SuchPagerAdapter(FragmentManager fragmentManager) {
        mFragmentManager = fragmentManager;
        mFragments = new SparseArray<>();
    }

    @Override
    public Object instantiateItem(ViewGroup container, int position) {
        Fragment fragment = getItem(position);
        if (mCurTransaction == null) {
            mCurTransaction = mFragmentManager.beginTransaction();
        }
        mCurTransaction.add(container.getId(),fragment,"fragment:"+position);
        return fragment;
    }

    @Override
    public void destroyItem(ViewGroup container, int position, Object object) {
        if (mCurTransaction == null) {
            mCurTransaction = mFragmentManager.beginTransaction();
        }
        mCurTransaction.detach(mFragments.get(position));
        mFragments.remove(position);
    }

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

    public Fragment getItem(int position) {         
        return YoursVeryFragment.instantiate();
    }

    @Override
    public void finishUpdate(ViewGroup container) {
        if (mCurTransaction != null) {
            mCurTransaction.commitAllowingStateLoss();
            mCurTransaction = null;
            mFragmentManager.executePendingTransactions();
        }
    }


    @Override
    public int getCount() {
        return countOfPages;
    }

}
7
votes

Why solutions above so complex? Looks like overkill. I solve it just with replacing old reference to new in class extended from FragmentPagerAdapter

 @Override
public Object instantiateItem(ViewGroup container, int position) {
    frags[position] = (Fragment) super.instantiateItem(container, position);
    return frags[position];
}

All adapter's code looks like this

public class RelationsFragmentsAdapter extends FragmentPagerAdapter {

private final String titles[] = new String[3];
private final Fragment frags[] = new Fragment[titles.length];

public RelationsFragmentsAdapter(FragmentManager fm) {
    super(fm);
    frags[0] = new FriendsFragment();
    frags[1] = new FriendsRequestFragment();
    frags[2] = new FriendsDeclinedFragment();

    Resources resources = AppController.getAppContext().getResources();

    titles[0] = resources.getString(R.string.my_friends);
    titles[1] = resources.getString(R.string.my_new_friends);
    titles[2] = resources.getString(R.string.followers);
}

@Override
public CharSequence getPageTitle(int position) {
    return titles[position];
}

@Override
public Fragment getItem(int position) {
    return frags[position];
}

@Override
public int getCount() {
    return frags.length;
}

@Override
public Object instantiateItem(ViewGroup container, int position) {
    frags[position] = (Fragment) super.instantiateItem(container, position);
    return frags[position];
}

}

0
votes

My code :

    public class SampleAdapter extends FragmentStatePagerAdapter {

    private Fragment mFragmentAtPos2;
    private FragmentManager mFragmentManager;
    private Fragment[] mFragments = new Fragment[3];


    public SampleAdapter(FragmentManager mgr) {
        super(mgr);
        mFragmentManager = mgr;
        Bundle hack = new Bundle();
        try {
            for (int i = 0; i < mFragments.length; i++) {
                hack.putInt("hack", i);
                mFragments[i] = mFragmentManager.getFragment(hack, "hack");
            }
        } catch (Exception e) {
            // No need to fail here, likely because it's the first creation and mActive is empty
        }

    }

    public void switchFrag(Fragment frag) {

        if (frag == null) {
            Dbg.e(TAG, "- switch(frag) frag is NULL");
            return;
        } else Dbg.v(TAG, "- switch(frag) - frag is " + frag.getClass());
        // We have to check for mFragmentAtPos2 null in case of first time (only Mytrips fragment being instatiante).
        if (mFragmentAtPos2!= null) 
            mFragmentManager.beginTransaction()  
                .remove(mFragmentAtPos2)
                .commit();
        mFragmentAtPos2 = frag;
        notifyDataSetChanged();
    }

    @Override
    public int getCount() {
        return(3);
    }

    @Override
    public int getItemPosition(Object object) {
        Dbg.v(TAG,"getItemPosition : "+object.getClass());
        if (object instanceof MyTripsFragment
                || object instanceof FindingDriverFragment
                || object instanceof BookingAcceptedFragment
                || object instanceof RideStartedFragment
                || object instanceof RideEndedFragment
                || object instanceof ContactUsFragment
                )
            return POSITION_NONE;
        else return POSITION_UNCHANGED;

    }

    @Override
    public Fragment getItem(int position) {
        Dbg.v("SampleAdapter", "getItem called on: "+position);
        switch (position) {
        case 0: 
            if (snapbookFrag==null) {
                snapbookFrag = new SnapBookingFragment();
                Dbg.e(TAG, "snapbookFrag created");
            }
            return snapbookFrag;
        case 1: 
            if(bookingFormFrag==null) {
                bookingFormFrag = new BookingFormFragment();
                Dbg.e(TAG, "bookingFormFrag created");
            }
            return bookingFormFrag;
        case 2: 
            if (mFragmentAtPos2 == null) {
                myTripsFrag = new MyTripsFragment();
                mFragmentAtPos2 = myTripsFrag;
                return mFragmentAtPos2;
            }
            return mFragmentAtPos2;
        default:
            return(new SnapBookingFragment());
        }
    }
}
0
votes

With reference to simekadam's solution, mFragments isn't being populated in instantiateItem and needs mFragments.put(position, fragment); inside or you'll end up with this error: Trying to remove fragment from view gives me NullPointerException on mNextAnim.

0
votes

The problem is that getItem() in FragmentPageAdapter has a wrong name. It should have been named createItem(). Because the way it works the getItem() is for create fragments and it's not safe to call it to query/find a fragment.

My recomendation is to make a copy of current FragmentPagerAdapter and change it that way:

Add:

    public abstract Fragment createFragment(int position);

And change getItem to:

public Fragment getItem(int position) {
    if(containerId!=null) {
        final long itemId = getItemId(position);
        String name = makeFragmentName(containerId, itemId);
        return mFragmentManager.findFragmentByTag(name);
    } else {
        return null;
    }
}

Finally add that to instantiateItem:

    if(containerId==null)
        containerId = container.getId();
    else if(containerId!=container.getId())
        throw new RuntimeException("Container id not expected to change");

Complete code at this gist

I think that this implementation is safer and easier to use and also has same performance from the original adapter from Google engineers.