1

What I'm trying to achieve:

An Activity with a ViewPager that displays Fragments for a list of Objects in the Adapter (FragmentStatePagerAdapter).

Initially the Activity loads N (lets say 5) objects from the SQLite DB into the Adapter. These objects are chosen with some randomness.

When the user is reaching the end of the list, the activity shall load M (let M be 3) more objects from the DB, add them to the adapter and call notifyDataSetChanged(). When adding them, I check if the new Objects already exist in the list and if they do, the pre-existing one gets removed and the loaded one gets added to the list's tail.

Thus, I'm trying to achieve something like an infinite scrolling ViewPager (NOT a "circular" ViewPager as I want new Objects to be fetched constantly, instead of going back to the begging of the list).

I have some working code and included a sample for the pattern I'm following down bellow. However, I keep getting this exception and have no idea why:

IllegalStateException: Fragment MyObjectFragment{id...} is not currently in the FragmentManager
at android.app.FragmentManagerImpl.saveFragmentInstanceState(FragmentManager.java:553)
at android.support.v13.app.FragmentStatePagerAdapter.destroyItem(FragmentStatePagerAdapter.java:140)
at android.support.v4.ViewPager.populate(ViewPager.java:1002)
...

Code Sample:

The Activity:

public class MyActivitty extends FragmentActivity {

    public MyPagerAdapter adapter;
    public ViewPager pager;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);

        setContentView(R.layout.my_acyivity_layout);
        pager = (ViewPager) findViewById(R.id.viewPager);

        ArrayList<MyObject> myObjects = new ArrayList<MyObject>();

        // loadInitialObjectsFromDB(int N) goes to SQLite DB and loads the N first objects to show on the ViewPager...
        myObjects = loadInitialObjectsFromDB(5); 

        // Adapter will use the previously fetched objects
        adapter = new MyPagerAdapter(this, getFragmentManager(), myObjects); 

        pager.setAdapter(adapter);
        pager.setOffscreenPageLimit(2);

        // (...)
    }

    // (...)
}

The PagerAdapter:

public class MyPagerAdapter extends FragmentStatePagerAdapter implements
    ViewPager.OnPageChangeListener {

    private MyActivity context;
    private ArrayList<MyObject> objectList;
    private int currentPosition = 0;

    // (...)

    public MyPagerAdapter(MyActivity context, FragmentManager fragmentManager, ArrayList<MyObject> objects) 
    {
            super(fragmentManager);
            this.context = context;
            this.objectList = objects;
    }

    @Override
    public Fragment getItem(int position) 
    {
            MyObject object = objectList.get(position);
            return MyObjectFragment.newInstance(position, object);
    }

    @Override
    public int getItemPosition(Object object){
            MyObjectFragment frag = (MyObjectFragment) object;
            MyObject object = frag.getMyObject();

            for(int i = 0; i < objectList.size(); i++)
            {
                    if(objectList.get(i).getId() == object.getId())
                    {
                            return i;
                    }
            }

            return PagerAdapter.POSITION_NONE;
    }

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

    @Override
    public void onPageSelected(int position) 
    {
           currentPosition = position;
    }

    // (...)

}


@Override
public void onPageScrollStateChanged(int state) 
{   
    switch(state)
    {
    case ViewPager.SCROLL_STATE_DRAGGING:
            // (...)
        break;

    case ViewPager.SCROLL_STATE_IDLE:
         // if we are reaching the "end" of the list (while scrolling to the right), load more objects
            if(currentPosition <= position && position >= answerList.size()-1)
            {
                    // Function in MyActivity that fetches N more objects.
                    // and adds them to this adapter's ArrayList<MyObject> objectList
                    // it checks for duplicates so that if the fetched object was already in the back of the list, it is shown again
                    context.getMoreObjects(3); 
                    notifyDataSetChanged();
            }
        getMoreQuestions(currentPosition);



    case ViewPager.SCROLL_STATE_SETTLING:
        break;
    }

}

My Fragment:

public class MyObjectFragment extends Fragment {

    // Object to represent
    private MyObject object;

    public static Fragment newInstance(MyActivity context, 
         int position, MyObject object) {

            MyObjectFragment frag = new MyObjectFragment();

            Bundle args = new Bundle();
            args.putParcelable("Object", object);
            frag.setArguments(args);

            return frag;
    }

    @Override
    public View onCreateView(LayoutInflater inflater, ViewGroup container,
        Bundle savedInstanceState) {

            this.currentInflater = inflater;

            final View rootView = inflater.inflate(
                    R.layout.fragment_my_object, container, false);

            // get object from Bundle, set UI, events, etc...

    }

    // (...)

}

Any idea on why am I getting this Exception? It seems like the FragmentStatePagerAdapter is trying to destroy an item that no longer exists, but I don't understand why.

EDIT 1:

If I comment my @Override getItemPosition(Object object), I don't get the exception anymore. However, I need to override getItemPosition because there is a use case in which the user deletes the currently shown Object causing it to disappear from the adapter's array and forcing the getItemPosition to return POSITION_NONE if the item doesn't exist anymore.

EDIT 2:

Now I do know that this exception only happens when I remove items from my adapter's objectList. I have two situations where MyObject instances are deleted from the objectList:

  • When the getMoreObjects() adds fetches an object from the DB that was already in the objectList, I delete it and re-add it to the head of the list. I do this to avoid having objects with the same Id in the objectList, as their Id is used by the getItemPosition() to know if they exist and their position.

  • Before returning, getMoreObjects(), removes the N first objects from the list. I do know that the FragmentStatePagerAdapter already saves memory by only keeping in memory fragments for some of the objects, but I still would like to avoid growing my objectList too much. For now, I have this line commented, as it's not that important.

user1987392
  • 3,921
  • 4
  • 34
  • 59
  • You say `These objects are chosen with some randomness.` but then you fetch items with predefined position in the `objectList` for a position by using `objectList.get(position)`? I think your positions for objects get messed up which is creating the exception. – Shakti Feb 19 '14 at 13:08
  • @ShaktiThakran Yes, the objects are chosen from the DB with some randomness. When the user gets close to the end of the list, I get N more "random" objects from the DB and add them to the objectList. There is the possibility that I select an item that was already in the objectList, in which case I remove from wherever it is and add it again, to the tail of the list. By doing this, I know there are no "duplicates" (with the same object.Id) in the list as that could screw up the getItemPosition(). – user1987392 Feb 19 '14 at 13:18
  • Why not choose a different random object if you find it in the list instead of removing and then adding. I think you can do without the removal part. – Shakti Feb 19 '14 at 13:21
  • Because in this case it is acceptable that the item (MyObject) that was displayed in the past shows up again. I'll probably only do that as a last resource. – user1987392 Feb 19 '14 at 13:24
  • Ya that is possible in this case too, just cycle through the visible views. The ones that are removed because of the setOffscreenPageLimit() still will have a chance to reappear. The only ones you need to skip are which are already within the offscreenPageLimits. – Shakti Feb 19 '14 at 13:37

1 Answers1

3

Solved with the help of this question which itself points at this issue.

FragmentStatePagerAdapter caches the fragments and their saved states in two ArrayLists: mFragments and mSavedState. But when the fragments' order changes (as could happen in my case), there's no mechanism for reordering the elements of mFragments and mSavedState. Therefore, the adapter will provide the wrong fragments to the pager.

I've adapted the code provided in that and changed the import from app.support.v4.Fragment to android.app.Fragment.

public abstract class MyFragmentStatePagerAdapter extends PagerAdapter {
    private static final String TAG = "FragmentStatePagerAdapter";
    private static final boolean DEBUG = true;

    private final FragmentManager mFragmentManager;
    private FragmentTransaction mCurTransaction = null;

    private long[] mItemIds = new long[] {};
    private ArrayList<Fragment.SavedState> mSavedState = new ArrayList<Fragment.SavedState>();
    private ArrayList<Fragment> mFragments = new ArrayList<Fragment>();
    private Fragment mCurrentPrimaryItem = null;

    public MyFragmentStatePagerAdapter(FragmentManager fm) {
        mFragmentManager = fm;
        mItemIds = new long[getCount()];
        for (int i = 0; i < mItemIds.length; i++) {
            mItemIds[i] = getItemId(i);
        }
    }

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

    /**
     * Return a unique identifier for the item at the given position.
     */
    public int getItemId(int position) {
        return position;
    }

    @Override
    public void notifyDataSetChanged() {
        long[] newItemIds = new long[getCount()];
        for (int i = 0; i < newItemIds.length; i++) {
            newItemIds[i] = getItemId(i);
        }

        if (!Arrays.equals(mItemIds, newItemIds)) {
            ArrayList<Fragment.SavedState> newSavedState = new ArrayList<Fragment.SavedState>();
            ArrayList<Fragment> newFragments = new ArrayList<Fragment>();

            for (int oldPosition = 0; oldPosition < mItemIds.length; oldPosition++) {
                int newPosition = POSITION_NONE;
                for (int i = 0; i < newItemIds.length; i++) {
                    if (mItemIds[oldPosition] == newItemIds[i]) {
                        newPosition = i;
                        break;
                    }
                }
                if (newPosition >= 0) {
                    if (oldPosition < mSavedState.size()) {
                        Fragment.SavedState savedState = mSavedState.get(oldPosition);
                        if (savedState != null) {
                            while (newSavedState.size() <= newPosition) {
                                newSavedState.add(null);
                            }
                            newSavedState.set(newPosition, savedState);
                        }
                    }
                    if (oldPosition < mFragments.size()) {
                        Fragment fragment = mFragments.get(oldPosition);
                        if (fragment != null) {
                            while (newFragments.size() <= newPosition) {
                                newFragments.add(null);
                            }
                            newFragments.set(newPosition, fragment);
                        }
                    }
                }
            }

            mItemIds = newItemIds;
            mSavedState = newSavedState;
            mFragments = newFragments;
        }

        super.notifyDataSetChanged();
    }

    @Override
    public void startUpdate(ViewGroup container) {
    }

    @Override
    public Object instantiateItem(ViewGroup container, int position) {
        // 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 (mFragments.size() > position) {
            Fragment f = mFragments.get(position);
            if (f != null) {
                return f;
            }
        }

        if (mCurTransaction == null) {
            mCurTransaction = mFragmentManager.beginTransaction();
        }

        Fragment fragment = getItem(position);
        if (DEBUG) Log.v(TAG, "Adding item #" + position + ": f=" + fragment);
        if (mSavedState.size() > position) {
            Fragment.SavedState fss = mSavedState.get(position);
            if (fss != null) {
                fragment.setInitialSavedState(fss);
            }
        }
        while (mFragments.size() <= position) {
            mFragments.add(null);
        }
        fragment.setMenuVisibility(false);
        mFragments.set(position, fragment);
        mCurTransaction.add(container.getId(), fragment);

        return fragment;
    }

    public void destroyItemState(int position) {
        mFragments.remove(position);
        mSavedState.remove(position);
    }

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

        if (mCurTransaction == null) {
            mCurTransaction = mFragmentManager.beginTransaction();
        }

        //position = getItemPosition(object);
        if (DEBUG) Log.v(TAG, "Removing item #" + position + ": f=" + object
                + " v=" + ((Fragment)object).getView());
        if (position >= 0) {
            while (mSavedState.size() <= position) {
                mSavedState.add(null);
            }
            mSavedState.set(position, mFragmentManager.saveFragmentInstanceState(fragment));
            if(position < mFragments.size()){
                mFragments.set(position, null);
            }
        }

        mCurTransaction.remove(fragment);
    }

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

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

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

    @Override
    public Parcelable saveState() {
        Bundle state = new Bundle();
        if (mItemIds.length > 0) {
            state.putLongArray("itemids", mItemIds);
        }
        if (mSavedState.size() > 0) {
            Fragment.SavedState[] fss = new Fragment.SavedState[mSavedState.size()];
            mSavedState.toArray(fss);
            state.putParcelableArray("states", fss);
        }
        for (int i=0; i<mFragments.size(); i++) {
            Fragment f = mFragments.get(i);
            if (f != null) {
                String key = "f" + i;
                mFragmentManager.putFragment(state, key, f);
            }
        }
        return state;
    }

    @Override
    public void restoreState(Parcelable state, ClassLoader loader) {
        if (state != null) {
            Bundle bundle = (Bundle)state;
            bundle.setClassLoader(loader);
            mItemIds = bundle.getLongArray("itemids");
            if (mItemIds == null) {
                mItemIds = new long[] {};
            }
            Parcelable[] fss = bundle.getParcelableArray("states");
            mSavedState.clear();
            mFragments.clear();
            if (fss != null) {
                for (int i=0; i<fss.length; i++) {
                    mSavedState.add((Fragment.SavedState)fss[i]);
                }
            }
            Iterable<String> keys = bundle.keySet();
            for (String key: keys) {
                if (key.startsWith("f")) {
                    int index = Integer.parseInt(key.substring(1));
                    Fragment f = mFragmentManager.getFragment(bundle, key);
                    if (f != null) {
                        while (mFragments.size() <= index) {
                            mFragments.add(null);
                        }
                        f.setMenuVisibility(false);
                        mFragments.set(index, f);
                    } else {
                        Log.w(TAG, "Bad fragment at key " + key);
                    }
                }
            }
        }
    }
}

Credit for the original code goes to user @UgglyNoodle.

Then, instead of using FragmentStatePagerAdapter I use the MyFragmentStatePagerAdapter from above and override getItemPosition() and getItemId() consistently with getItem().

Community
  • 1
  • 1
user1987392
  • 3,921
  • 4
  • 34
  • 59
  • Hey there, it is not clear to me how you use this version of the adapter, could you please clarify? – gwvatieri Mar 12 '14 at 23:41
  • I have an Activity whose View has a ViewPager that shows some fragments. In that Activity and for that ViewPager, I have an adapter (let's call it MyViewPagerAdapter) that instead of extending FragmentStatePagerAdapter or FragmentPagerAdapter (the "normal" procedure) extends "my" custom class MyFragmentStatePagerAdapter (in the Answer). In my MyViewPagerAdapter, I override the getItemPosition() and getItemId() so that they are consistent with the getItem() method. Hope this helps! – user1987392 Mar 12 '14 at 23:48
  • Thanks for following up. What I do not understand is "I override the getItemPosition() and getItemId() so that they are consistent with the getItem() method" – gwvatieri Mar 12 '14 at 23:49
  • I don't remember what exactly I meant with that but I think it's: getItem(i) must return item X on position i and at the same time getItemPosition(X) must return i as the position (or POSITION_NONE if the item was removed). getItemId(X) must return a unique id for the object X. – user1987392 Mar 13 '14 at 11:31