Remove Fragment Page from ViewPager in Android

106,349

Solution 1

The solution by Louth was not enough to get things working for me, as the existing fragments were not getting destroyed. Motivated by this answer, I found that the solution is to override the getItemId(int position) method of FragmentPagerAdapter to give a new unique ID whenever there has been a change in the expected position of a Fragment.

Source Code:

private class MyPagerAdapter extends FragmentPagerAdapter {

    private TextProvider mProvider;
    private long baseId = 0;

    public MyPagerAdapter(FragmentManager fm, TextProvider provider) {
        super(fm);
        this.mProvider = provider;
    }

    @Override
    public Fragment getItem(int position) {
        return MyFragment.newInstance(mProvider.getTextForPosition(position));
    }

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


    //this is called when notifyDataSetChanged() is called
    @Override
    public int getItemPosition(Object object) {
        // refresh all fragments when data set changed
        return PagerAdapter.POSITION_NONE;
    }


    @Override
    public long getItemId(int position) {
        // give an ID different from position when position has been changed
        return baseId + position;
    }

    /**
     * Notify that the position of a fragment has been changed.
     * Create a new ID for each position to force recreation of the fragment
     * @param n number of items which have been changed
     */
    public void notifyChangeInPosition(int n) {
        // shift the ID returned by getItemId outside the range of all previous fragments
        baseId += getCount() + n;
    }
}

Now, for example if you delete a single tab or make some change to the order, you should call notifyChangeInPosition(1) before calling notifyDataSetChanged(), which will ensure that all the Fragments will be recreated.

Why this solution works

Overriding getItemPosition():

When notifyDataSetChanged() is called, the adapter calls the notifyChanged() method of the ViewPager which it is attached to. The ViewPager then checks the value returned by the adapter's getItemPosition() for each item, removing those items which return POSITION_NONE (see the source code) and then repopulating.

Overriding getItemId():

This is necessary to prevent the adapter from reloading the old fragment when the ViewPager is repopulating. You can easily understand why this works by looking at the source code for instantiateItem() in FragmentPagerAdapter.

    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);
        if (DEBUG) Log.v(TAG, "Adding item #" + itemId + ": f=" + fragment);
        mCurTransaction.add(container.getId(), fragment,
                makeFragmentName(container.getId(), itemId));
    }

As you can see, the getItem() method is only called if the fragment manager finds no existing fragments with the same Id. To me it seems like a bug that the old fragments are still attached even after notifyDataSetChanged() is called, but the documentation for ViewPager does clearly state that:

Note this class is currently under early design and development. The API will likely change in later updates of the compatibility library, requiring changes to the source code of apps when they are compiled against the newer version.

So hopefully the workaround given here will not be necessary in a future version of the support library.

Solution 2

The ViewPager doesn't remove your fragments with the code above because it loads several views (or fragments in your case) into memory. In addition to the visible view, it also loads the view to either side of the visible one. This provides the smooth scrolling from view to view that makes the ViewPager so cool.

To achieve the effect you want, you need to do a couple of things.

  1. Change the FragmentPagerAdapter to a FragmentStatePagerAdapter. The reason for this is that the FragmentPagerAdapter will keep all the views that it loads into memory forever. Where the FragmentStatePagerAdapter disposes of views that fall outside the current and traversable views.

  2. Override the adapter method getItemPosition (shown below). When we call mAdapter.notifyDataSetChanged(); the ViewPager interrogates the adapter to determine what has changed in terms of positioning. We use this method to say that everything has changed so reprocess all your view positioning.

And here's the code...

private class MyPagerAdapter extends FragmentStatePagerAdapter {

    //... your existing code

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

}

Solution 3

my working solution to remove fragment page from view pager

public class MyFragmentAdapter extends FragmentStatePagerAdapter {

    private ArrayList<ItemFragment> pages;

    public MyFragmentAdapter(FragmentManager fragmentManager, ArrayList<ItemFragment> pages) {
        super(fragmentManager);
        this.pages = pages;
    }

    @Override
    public Fragment getItem(int index) {
        return pages.get(index);
    }

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

    @Override
    public int getItemPosition(Object object) {
        int index = pages.indexOf (object);

        if (index == -1)
            return POSITION_NONE;
        else
            return index;
    }
}

And when i need to remove some page by index i do this

pages.remove(position); // ArrayList<ItemFragment>
adapter.notifyDataSetChanged(); // MyFragmentAdapter

Here it is my adapter initialization

MyFragmentAdapter adapter = new MyFragmentAdapter(getSupportFragmentManager(), pages);
viewPager.setAdapter(adapter);

Solution 4

The fragment must be already removed but the issue was viewpager save state

Try

myViewPager.setSaveFromParentEnabled(false);

Nothing worked but this solved the issue !

Cheers !

Solution 5

I had the idea of simply copy the source code from android.support.v4.app.FragmentPagerAdpater into a custom class named CustumFragmentPagerAdapter. This gave me the chance to modify the instantiateItem(...) so that every time it is called, it removes / destroys the currently attached fragment before it adds the new fragment received from getItem() method.

Simply modify the instantiateItem(...) in the following way:

@Override
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);

    // remove / destroy current fragment
    if (fragment != null) {
        mCurTransaction.remove(fragment);
    }

    // get new fragment and add it
    fragment = getItem(position);
    mCurTransaction.add(container.getId(), fragment,    makeFragmentName(container.getId(), itemId));

    if (fragment != mCurrentPrimaryItem) {
        fragment.setMenuVisibility(false);
        fragment.setUserVisibleHint(false);
    }

    return fragment;
}
Share:
106,349
astuetz
Author by

astuetz

Android developer at Runtastic, UI ninja

Updated on August 07, 2020

Comments

  • astuetz
    astuetz almost 4 years

    I'm trying to dynamically add and remove Fragments from a ViewPager, adding works without any problems, but removing doesn't work as expected.

    Everytime I want to remove the current item, the last one gets removed.

    I also tried to use an FragmentStatePagerAdapter or return POSITION_NONE in the adapter's getItemPosition method.

    What am I doing wrong?

    Here's a basic example:

    MainActivity.java

    public class MainActivity extends FragmentActivity implements TextProvider {
    
        private Button mAdd;
        private Button mRemove;
        private ViewPager mPager;
    
        private MyPagerAdapter mAdapter;
    
        private ArrayList<String> mEntries = new ArrayList<String>();
    
        @Override
        public void onCreate(Bundle savedInstanceState) {
            super.onCreate(savedInstanceState);
            setContentView(R.layout.main);
    
            mEntries.add("pos 1");
            mEntries.add("pos 2");
            mEntries.add("pos 3");
            mEntries.add("pos 4");
            mEntries.add("pos 5");
    
            mAdd = (Button) findViewById(R.id.add);
            mRemove = (Button) findViewById(R.id.remove);
            mPager = (ViewPager) findViewById(R.id.pager);
    
            mAdd.setOnClickListener(new OnClickListener() {
                @Override
                public void onClick(View view) {
                    addNewItem();
                }
            });
    
            mRemove.setOnClickListener(new OnClickListener() {
                @Override
                public void onClick(View view) {
                    removeCurrentItem();
                }
            });
    
            mAdapter = new MyPagerAdapter(this.getSupportFragmentManager(), this);
    
            mPager.setAdapter(mAdapter);
    
        }
    
        private void addNewItem() {
            mEntries.add("new item");
            mAdapter.notifyDataSetChanged();
        }
    
        private void removeCurrentItem() {
            int position = mPager.getCurrentItem();
            mEntries.remove(position);
            mAdapter.notifyDataSetChanged();
        }
    
        @Override
        public String getTextForPosition(int position) {
            return mEntries.get(position);
        }
        @Override
        public int getCount() {
            return mEntries.size();
        }
    
    
        private class MyPagerAdapter extends FragmentPagerAdapter {
    
            private TextProvider mProvider;
    
            public MyPagerAdapter(FragmentManager fm, TextProvider provider) {
                super(fm);
                this.mProvider = provider;
            }
    
            @Override
            public Fragment getItem(int position) {
                return MyFragment.newInstance(mProvider.getTextForPosition(position));
            }
    
            @Override
            public int getCount() {
                return mProvider.getCount();
            }
    
        }
    
    }
    

    TextProvider.java

    public interface TextProvider {
        public String getTextForPosition(int position);
        public int getCount();
    }
    

    MyFragment.java

    public class MyFragment extends Fragment {
    
        private String mText;
    
        public static MyFragment newInstance(String text) {
            MyFragment f = new MyFragment(text);
            return f;
        }
    
        public MyFragment() {
        }
    
        public MyFragment(String text) {
            this.mText = text;
        }
    
        @Override
        public View onCreateView(LayoutInflater inflater, ViewGroup container,
                Bundle savedInstanceState) {
    
            View root = inflater.inflate(R.layout.fragment, container, false);
    
            ((TextView) root.findViewById(R.id.position)).setText(mText);
    
            return root;
        }
    
    }
    

    activity_main.xml

    <?xml version="1.0" encoding="utf-8"?>
    <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:orientation="vertical" >
    
        <Button
            android:id="@+id/add"
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:text="add new item" />
    
        <Button
            android:id="@+id/remove"
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:text="remove current item" />
    
        <android.support.v4.view.ViewPager
            android:id="@+id/pager"
            android:layout_width="match_parent"
            android:layout_height="0dip"
            android:layout_weight="1" />
    
    </LinearLayout>
    

    fragment.xml

    <?xml version="1.0" encoding="utf-8"?>
    <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:orientation="vertical" >
    
        <TextView
            android:id="@+id/position"
            android:layout_width="match_parent"
            android:layout_height="match_parent"
            android:gravity="center"
            android:textSize="35sp" />
    
    </LinearLayout>
    
    • wizurd
      wizurd almost 10 years
      +1 for the source code
    • codeKiller
      codeKiller almost 7 years
      is it correct to use non-default constructor in MyFragment.java??
  • C--
    C-- over 11 years
    Ok, this one seems to be the easiest workaround for this issue. Although a couple of other workarounds are discussed on the bug reports here : code.google.com/p/android/issues/detail?id=19110 and here : code.google.com/p/android/issues/detail?id=19001
  • gcl1
    gcl1 over 11 years
    @Louth: I am using ViewPager with TabsAdapter, as extended from FragmentStatePagerAdapter (per example on Google ref page: developer.android.com/reference/android/support/v4/view/…). My follow up question is: how do you delete a tab in the first place? Thanks.
  • syloc
    syloc about 11 years
    @Louth When we call notifyDataSetChanged() the viewpager creates new fragments, but the older ones are still in memory. And my problem is; they receive a call for their onOptionsItemSelected event. How can i get rid of the old fragments?
  • Louth
    Louth about 11 years
    I don't feel that comments are the correct place for follow up questions. If you have questions related to this one, please post a new question with a reference to this thread.
  • Michal
    Michal about 11 years
    Thank you, just replacing FragmentPagerAdapter to a FragmentStatePagerAdapter solved my problem. After that I just removeAllViews() on ViewPager and reload fragments again dynamically.
  • Stavros Korokithakis
    Stavros Korokithakis about 11 years
    Thank you for this, this has been driving me nuts!
  • mohitum
    mohitum almost 11 years
    Thanks for the answer, i have wasted 2 days to do but your answer helped me to solve it within 2 minutes....Thanks very much
  • AndroidLearner
    AndroidLearner over 10 years
    @Louth Can you please help me on this .... m in great need please help me. stackoverflow.com/questions/21351331/…
  • Zordid
    Zordid over 10 years
    this is the exact opposite to the accepted answer - how can this work??
  • Zordid
    Zordid over 10 years
    Why are you always returning POSITION_NONE instead of really looking if a given view/fragment/object still exists and is valid? This method is supposed to answer the question "hey, can I reuse this one?" - and always saying "No!" just means recreation of everything! This is unnecessary in many cases.
  • egfconnor
    egfconnor about 10 years
    I agree with @Zordid. In my case with a bunch of high res pictures being swiped through it would pause for a second or so to recreate views which is not necessary. Switching to the getItemPosition() from stackoverflow.com/a/13671777/1212314 is a better solution.
  • Hamzeh Soboh
    Hamzeh Soboh almost 10 years
    Thanks!! You saved my day.
  • Tim Rae
    Tim Rae over 9 years
    This wasn't enough to get things working for me, see my answer
  • u2tall
    u2tall over 9 years
    Thanks so much for this answer! It has been grueling trying to solve this issue without using FragmentStatePagerAdapter. Great explanation too. Have you found a way to get getItem to trigger when coming back to the fragment? I've tried using your clever notifyDataSetChanged() in various places (ie onStop) without success
  • Tim Rae
    Tim Rae over 9 years
    I don't really understand your question... Please post a new question including source code if you can't figure it out.
  • Aparupa Ghoshal
    Aparupa Ghoshal about 9 years
    :) Thank you for this solution.
  • BitByteDog
    BitByteDog almost 9 years
    This only works if you are deleting the far right tab, to delete an inner tab or the left tab, you must reorder the tabs and return the new position as stated in the documentation developer.android.com/reference/android/support/v4/view/…
  • mehdok
    mehdok over 8 years
    using @Override public int getItemPosition (Object object) { int index = mFragments.indexOf (object); if (index == -1) return POSITION_NONE; else return index; } makes more sense and also works for me.
  • Skizo-ozᴉʞS
    Skizo-ozᴉʞS over 8 years
    Reminder for my quesiton, have you seen the question or you were busy @Louth??
  • C--
    C-- about 8 years
    2016, this workaround is still required to make notifyDataSetChanged() to work.
  • Majid
    Majid almost 8 years
    Thanks you save me.
  • rupps
    rupps over 7 years
    Wow! Amazing explanation. I had a pretty complex viewpager where I remove and insert items dynamically and wouldn't get it to work without understanding all this.
  • kopikaokao
    kopikaokao over 7 years
    Still need this workaround to replace old fragment. Dang it.
  • Zinc
    Zinc over 7 years
    I was using another workaround that has been busted in support library 25, but this is working like a charm. I only wish to give you more than one upvote. Thanks, mate!
  • VVB
    VVB over 7 years
    Will you please share code for addition of fragment?
  • VVB
    VVB over 7 years
    But while adding it reflects slow after swipe twice/thrice
  • Vasil Valchev
    Vasil Valchev over 7 years
    if you are not removing items, it only return item, nothing more... are you testing on vm or real device?
  • Vasil Valchev
    Vasil Valchev over 7 years
    Try other solutions, answers whit more up votes. View pager has very complex cashing system for better performance on swip, so this hacks are not handled by original view pager whit purpose...
  • Janice Kartika
    Janice Kartika about 7 years
    2017 now. And I still want to say thank you for this solution.
  • DearVolt
    DearVolt almost 7 years
    I didn't even have to override getItemPosition() for this to work for me. FragmentStatePagerAdapter is exactly what I was looking for.
  • Stamatis Stiliats
    Stamatis Stiliats over 6 years
    is this solved in any other way at 2018? Or we need workarounds?
  • Rahul Patil
    Rahul Patil over 5 years
    can you please give me source code because above link not open
  • Albert Nguyen
    Albert Nguyen about 5 years
    after few hours find other solution, I visited here again, and change FragmentPagerAdapter to FragmentStatePagerAdapter like above. Help me solved my problem about remove tab and fragment from tablayout and Viewpager. This is best solution.
  • Fazal Hussain
    Fazal Hussain over 4 years
    2020 now. Still, This is working as expected +1 upvote
  • Bhavin Patel
    Bhavin Patel almost 4 years
    I have 2 fragments mainfragment and otherfragment . I'm adding otherfragment after every 5th position and it's not in arraylist so how to remove that otherfragment if its not in list ? I'm using viewpager2
  • Bhavin Patel
    Bhavin Patel almost 4 years
    I have 2 fragments mainfragment and otherfragment . I'm adding otherfragment after every 5th position and it's not in arraylist so how to remove that otherfragment if its not in list ? I'm using viewpager2
  • Bhavin Patel
    Bhavin Patel almost 4 years
    I have 2 fragments mainfragment and otherfragment . I'm adding otherfragment after every 5th position and it's not in arraylist so how to remove that otherfragment if its not in list ? I'm using viewpager2