4

I'm using ViewPager and FragmentPagerAdapter within a detail-view. This means I have a list of items and one screen shows the details of a single item. But the user can swipe left and right to navigate through all items. This follows the Google guideline for swiping views.

But I wonder about one thing. Within a ListView the views for each row get re-used. Once a row scrolls out of the screen it is re-used as the convertView parameter of the getView method of the adapter that is bound to the ListView. But this re-usage behavior does not seem to be implemented for swiping views. This example illustrates this:

class DemoAdapter extends ArrayAdapter<DemoItem> {

    public DemoAdapter(Context context, List<DemoItem> objects) {
        super(context, 0, objects);
    }

    @Override
    public View getView(int position, View convertView, ViewGroup parent) {

        if (convertView == null) {
            // create a new view, otherwise re-use the existing convertView
            LayoutInflater i = (LayoutInflater) getContext().getSystemService(Context.LAYOUT_INFLATER_SERVICE);
            convertView = i.inflate(R.layout.list_item_demo, parent, false);
        }

        // get current item
        DemoItem item = getItem(position);

        if (item == null)
            return convertView;

        // update view with the item
        TextView textTitle = (TextView)convertView.findViewById(R.id.demo_title);
        if (textTitle != null)
            textTitle.setText(item.getTitle());

        return convertView;
    }
}

But here's the problem: Both, the FragmentPagerAdapter and the FragmentStatePagerAdapter are creating the fragments (each screen is a fragment) in their getItem method. But they don't get old fragments as an input parameter. The only difference is, that the FragmentStatePagerAdapter destroys unused fragments.

public class DemoItemsPagerAdapter extends FragmentPagerAdapter {

    private final Context context;

    public DemoItemsPagerAdapter(FragmentManager fm, Context context) {
        super(fm);    
        this.context = context;

        // ToDo: get cursor or array of available items that can be swiped through
    }

    @Override
    public Fragment getItem(int i) {

        Fragment fragment = new DemoItemFragment();

        // ToDo: initialize fragment by correct item
        // ToDo: avoid creating too many fragments - try reusing them (but how?)

        return fragment;
    }

    @Override
    public Object instantiateItem(ViewGroup container, int position) {

        // the container is not the fragment, but the ViewPager itself
        return super.instantiateItem(container, position);
    }

    @Override
    public CharSequence getPageTitle(int position) {    
        // ToDo: return name for current entry
        return null;
    }

    @Override
    public int getCount() {    
        // ToDo: get count from cursor/array of available items
        return 2;
    }
}

So, how can I reuse the fragments? Actually getItems should only be called twice because there is only one fragment visible at a time and a second one once the transition starts while the user is swiping.

UPDATE: Because of confusion, I created this drawing. It shows the behavior of the adapters. The default one keeps all fragments in memory unless the device runs out of memory. One the app is in background or killed and then restored each fragment will be restored from its SavedInstanceState. The second implementation keeps only some fragments in memory but if you swipe left/right the destroyed ones will be completely created again from scratch. The third implementation is what I'm seeking. You have only three fragments which are then reused when swiping left or right. So fragment A can be position 1, 2, 3, 4, 5, 6 and 7.

FragmentPagerAdapter implementations

Matthias
  • 5,574
  • 8
  • 61
  • 121
  • Why would you want to reuse fragments? A listview reuses views because then it doesn't have to inflate them again and again, which is supposed to be expensive. I for myself think reusing views is overrated. You would have to program the reusing of fragments yourself, which is a non trivial effort. Why bother? – Christine Jul 06 '15 at 23:49
  • 5
    Because each page looks exactly the same and inflating layouts is indeed an expensive operation. – Matthias Jul 07 '15 at 06:10

1 Answers1

0

Firstly to answer your question.

So, how can I reuse the fragments?

You can maintain an array of fragments in a SparseArray (It is more memory efficient than a HashMap when you need to map objects to integers).

private SparseArray<BaseFragment> fragments;

So your code can be something like this in the getItem(int i).

@Override
    public Fragment getItem(int position) {
        // Create a new fragment only when the fragment is not present in the fragments sparse
        // array
        BaseFragment fragment = fragments.get(position);
        if (fragment == null) {
            switch (position) {
                case 0: {
                    fragment = new Fragment1();
                    fragments.put(0, fragment);
                    break;
                }

                case 1: {
                    fragment = new Fragment2();
                    fragments.put(1, fragment);
                    break;
                }

                .
                .
                .

                default:
                    fragment = null;
            }
        }
        return fragment;
    }

Here I use a BaseFragment which is extended by almost all the fragments that I want to use.

And AFAIK, getItem() is called based upon the offScreenPageLimit. The default is 1. So based upon this number, the fragments that will be kept in memory will be

1 + 2*offScreenPageLimit // Current Page + 2 * left/right items

or

1 + offScreenPageLimit // if its the first or last page.

UPDATE 1

You dont have to worry about handling the removal of fragments from the memory. The FragmentStatePagerAdapter automatically handles that for you as mentioned in their docs.

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 FragmentPagerAdapter at the cost of potentially more overhead when switching between pages.

You use FragmentPagerAdapter when you've less pages to swipe, generally when using tabs or when the fragments are static. The doc says,

This version of the pager is best for use when there are a handful of typically more static fragments to be paged through, such as a set of tabs. The fragment of each page the user visits will be kept in memory, though its view hierarchy may be destroyed when not visible. This can result in using a significant amount of memory since fragment instances can hold on to an arbitrary amount of state. For larger sets of pages, consider FragmentStatePagerAdapter.

UPDATE 2
Your definition above for case 2 is wrong.

The second implementation keeps only some fragments in memory but if you swipe left/right the destroyed ones will be completely created again from scratch.

It wont be created from scratch, it will save the state of the previously created fragment and use the same state while creating the new one. You can look at the source code here to better understand how it works.

As to your 3rd implementation, I'd suggest overriding the default behavior of the adapter and then manually removing/adding view from the adapter based upon the current position.

Community
  • 1
  • 1
Antrromet
  • 15,294
  • 10
  • 60
  • 75
  • but how can I ceanup the array of some fragments are not in use anymore. and what's different for the FragmentStatePagerAdapter? – Matthias Jul 06 '15 at 17:51
  • I know the docs. but as long as you are holding the views in your SparseArray, then they will never be destroyed but remain in memory. I will try using your offset calculation and keep only those in the array. – Matthias Jul 06 '15 at 18:16
  • I use SparseArrays generally when I want to access to the instance of my fragment at a particular position. FragmentStatePagerAdapter destroys the items in the memory but keeps their state, so the next time when the fragment is inflated, it can restore back to the state. The sad thing is (afaik) that there is no method that gets called when the fragment is removed from the adapters memory. – Antrromet Jul 06 '15 at 18:30
  • Also take a look at http://stackoverflow.com/a/9646622/451951 and http://stackoverflow.com/questions/9727173. – Antrromet Jul 06 '15 at 18:32
  • So I read through the other questions that you mentioned and all say it's bad practice to hold a reference to fragments in your Adapter. instead a fragment is created for each position and the FragmentStatePagerAdapter keeps at least two of them in memory but destroys the others. But while swiping it create new ones and destroys others. In FragmentPagerAdapter you will get the old one for pos 0, 1, etc because they are still in memory. But none of the solutions creates ONLY 2 fragments. – Matthias Jul 06 '15 at 22:32
  • I dont think there will ever be a case with ONLY 2 fragments, because in a viewpager you need to have ATLEAST 3. The current one, previous and next. I guess you're forgetting the fact that in the viewpager the user can also go back the page. – Antrromet Jul 06 '15 at 22:35
  • True, 3 are perfect. But nobody needs 10 or 100 or even 3 that are destroyed and 3 are created again (1 by 1) when swiping. – Matthias Jul 06 '15 at 22:39
  • Let us [continue this discussion in chat](http://chat.stackoverflow.com/rooms/82545/discussion-between-antrromet-and-matthias). – Antrromet Jul 06 '15 at 22:41
  • I updated my question with an illustration. See above. – Matthias Jul 06 '15 at 22:58
  • Hello @Matthias, could you find any solution for 3) that you explained. I need exactly same.Mine is a weather app with max 10 pages. Each has SAME but huge no of views but with different data. Currently I used StatePagerAdapter with OffScreenlImit set to 9 so it is as if using FragPagerAdapter. Prob is it loads all the fragments while launching the app and takes nearly 10 secs. And frequently getting OOMs. Very bad situation it is at currently. I thought why can't this reuse view objects of a fragment in another fragment. Your help is much appreciated. – cgr Jan 28 '16 at 12:11
  • It should be exactly like RecyclerView. It should be able to reuse the View hierarchy of one fragment in another fragment. – cgr Jan 28 '16 at 12:16
  • @cgr No, unfortunately I didn't find a solution for that. I was expecting a behavior like a RecyclerView but it's not. I stayed with a FragementStatePagerAdapter so far. – Matthias Feb 03 '16 at 08:02
  • @Matthias, I have come across this. Try using it. I have not tried it yet oc course. https://github.com/lsjwzh/RecyclerViewPager – cgr Feb 29 '16 at 09:05
  • Thanks for the link. currently I'm not working on that topic anymore. but the link is worth trying. – Matthias Mar 03 '16 at 10:12