Search code examples
javaandroidandroid-fragmentsandroid-viewpagermaster-detail

Master-Detail View using ViewPager


I'd like to change the master-detail implementation of my Android phone app. Currently, users can select items from a ListView, opening a new activity. To select a different activity, the user must return to the list. Instead of this pogo-sticking, I'd like the user to swipe left and right to page through the documents using a ViewPager. There can be many documents, so I'd like to load at most 3 pages at a time - the current page, the previous, and the next. Paging back and forth should then add and remove pages left and right. I've created an adapter implementing FragmentStatePagerAdapter that handles static content (e.g. TextViews) nicely. Also deleting pages seems to work OK (not included here). But when I add e.g. an EditText content is copied over from one page to the next when paging.

Below is the code for the adapter and for the activity. There are two questions I have:

  1. What is wrong with my adapter that causes the undesired copying of EditText from one fragment to the next?
  2. This is my first shot at this, and it's probably far from an optimal implementation. But I find this to be such a common use case that I almost feel like there would be a ready made framework for it. Could this be achieved much easier?

Pager Adapter:

public class DetailPagerAdapter extends FragmentStatePagerAdapter {

    private final List<Fragment> mFragments;
    private final static String TAG = "DetailPagerAdapter";

    public DetailPagerAdapter(FragmentManager fm, List<Fragment> fragments) {
        super(fm);
        mFragments = fragments;
    }

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

    @Override
    public int getItemPosition(Object object) {
        return PagerAdapter.POSITION_NONE;
    }

    @Override
    public Fragment getItem(int position) {
        return mFragments.get(position);
    }

    public void addItem(Fragment fragment) {
        mFragments.add(fragment);
        notifyDataSetChanged();
    }

    public void removeItem(int position) {
        mFragments.remove(position);
        notifyDataSetChanged();
    }

    public void insertItem(int position, Fragment fragment) {
        mFragments.add(position, fragment);
        notifyDataSetChanged();
    }
}

PagingActivity Base Class:

public abstract class PagingActivity 
        extends AppCompatActivity
        implements ViewPager.OnPageChangeListener {

    protected ViewPager mViewPager;
    DetailPagerAdapter mViewPagerAdapter;
    protected ArrayList<String> mAllItemIds;
    private String mPreviousItemId;
    private String mCurrentItemId;
    private String mNextItemId;

    private boolean mMuteOnPageSelected = false;


    protected abstract Fragment getNewPageFragment(String id);

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

        List<Fragment> initialFragments = new ArrayList<>();

        int currentItemIndex = mAllItemIds.indexOf(mCurrentItemId);
        int pageSelection = 1;
        // Add previous view.
        if (currentItemIndex > 0) {
            mPreviousItemId = mAllItemIds.get(mAllItemIds.indexOf(mCurrentItemId) - 1);
            initialFragments.add(getNewPageFragment(mPreviousItemId));
        } else {
            pageSelection = 0;
            mPreviousItemId = null;
        }
        // Add current view.
        initialFragments.add(getNewPageFragment(mCurrentItemId));
        // Add next view.
        if (currentItemIndex < mAllItemIds.size() - 1) {
            mNextItemId = mAllItemIds.get(mAllItemIds.indexOf(mCurrentItemId) + 1);
            initialFragments.add(getNewPageFragment(mNextItemId));
        } else {
            mNextItemId = null;
        }

        mViewPagerAdapter = new DetailPagerAdapter(getSupportFragmentManager(), initialFragments);
        mViewPager.setAdapter(mViewPagerAdapter);
        mViewPager.setCurrentItem(pageSelection);

        mViewPager.addOnPageChangeListener(this);
    }

    @Override
    public void onPageSelected(int position) {

        if (!mMuteOnPageSelected) {
            mCurrentItemId = ((PagingFragment) (mViewPagerAdapter.getItem(mViewPager.getCurrentItem()))).getItemId();
            int currentItemIndex = mAllItemIds.indexOf(mCurrentItemId);

            // Navigated to the right.
            if (position == mViewPagerAdapter.getCount() - 1) {
                // Add next if not already pointing at the last available item.
                if (currentItemIndex < mAllItemIds.size() - 1) {
                    mNextItemId = mAllItemIds.get(mAllItemIds.indexOf(mCurrentItemId) + 1);
                    mViewPagerAdapter.addItem(getNewPageFragment(mNextItemId));
                } else {
                    mNextItemId = null;
                }
                // If it succeeds remove first item.
                int itemCount = mViewPagerAdapter.getCount();
                if ((itemCount > 3) || ((itemCount == 3) && (currentItemIndex == mAllItemIds.size() - 1))) {
                    mMuteOnPageSelected = true;
                    mViewPagerAdapter.removeItem(0);
                    mViewPager.setCurrentItem(1);
                    mMuteOnPageSelected = false;
                }
            }

            // Navigated to the left.
            else if (position == 0) {
                // Add item on the left if not already pointing at the first available item.
                if (currentItemIndex > 0) {
                    mPreviousItemId = mAllItemIds.get(mAllItemIds.indexOf(mCurrentItemId) - 1);
                    mViewPagerAdapter.insertItem(0, getNewPageFragment(mPreviousItemId));
                } else {
                    mPreviousItemId = null;
                }
                // Check if last item needs to be removed and selection updated.
                int itemCount = mViewPagerAdapter.getCount();
                if (itemCount == 3) {
                    if (currentItemIndex == 0) {
                        // Points to the first of two items.
                        // -> do not change selection
                        // -> remove rightmost item.
                        mViewPagerAdapter.removeItem(itemCount - 1);
                    } else if (currentItemIndex == mAllItemIds.size() - 2) {
                        // Will point to the middle of 3 items.
                        // -> nothing to remove
                        // -> select middle page.
                        mMuteOnPageSelected = true;
                        mViewPager.setCurrentItem(1);
                        mMuteOnPageSelected = false;
                    }
                } else if (itemCount > 3) {
                    // Pager contains 4 items, first item selected.
                    // -> remove rightmost item
                    // -> select middle page.
                    mMuteOnPageSelected = true;
                    mViewPagerAdapter.removeItem(itemCount - 1);
                    mViewPager.setCurrentItem(1);
                    mMuteOnPageSelected = false;
                }
            }

            mViewPagerAdapter.notifyDataSetChanged();
        }
    }
}

Solution

  • The second question was the key: Yes, at least the current state can be achieved much easier by letting the adapter handle the full array of items. FragmentStatePagerAdapter only loads as many fragments at a time as needed, so it can handle all the manual work I had done in the activity.

    Pager Adapter

    public class MyPagerAdapter extends FragmentStatePagerAdapter {
    
        private List<String> mAllItemIds;
    
        public MyPagerAdapter(Context context, FragmentManager fm) {
            super(fm);
            mAllItemIds = ...
        }
    
        @Override
        public int getCount() {
            return mAllItemIds.size();
        }
    
        @Override
        public int getItemPosition(Object object) {
            return PagerAdapter.POSITION_NONE;
        }
    
        @Override
        public Fragment getItem(int position) {
            return MyFragment.newInstance(mAllItemIds.get(position));
        }
    
        public void removeItem(int position) {
    
            // add needed code here to remove item also from source
            // ...
    
            mAllItemIds.remove(position);
            notifyDataSetChanged();
        }
    }
    

    Activity

    public abstract class PagingActivity extends AppCompatActivity {
    
        protected ViewPager mViewPager;
        MyPagerAdapter mViewPagerAdapter;
    
        @Override
        protected void onCreate(Bundle savedInstanceState) {
            super.onCreate(savedInstanceState);
    
            mViewPager = (ViewPager)findViewById(R.id.viewPager);
            mViewPagerAdapter = new MyPagerAdapter(this, getSupportFragmentManager());
            mViewPager.setAdapter(mViewPagerAdapter);
        }
    
        private void deleteItem() {
            mViewPagerAdapter.removeItem(mViewPager.getCurrentItem());
        }
    }