Lennon Petrick Lennon Petrick - 1 month ago 13
Android Question

ViewPager holds fragment's instance even after setting a new adapter

I'm facing some issues with

ViewPager
and
Fragment
's instances.

I have a
ViewPager
(let's call Father) with 4 fragments and into the last fragment I have another
ViewPager
(and call it Child) with dynamic fragments amount. What I mean is that I create the Child based on a list of objects in memory. So if the list contains 3 objets, the Child will have 3 fragments inside. If in a determinate moment something happens and I get a list with 1 object, then I must update the Child with just 1 fragment. An important point in here is that each Child's
Fragment
has its own object from the returned list and is created based on this object.

The code I do to set the list of fragments into the Child
ViewPager
is the following:

@Override
public void setViewPagerChildFragments(List<Fragment> fragments) {
if (fragments != null) {
DefaultStateViewPagerAdapter adapter = (DefaultStateViewPagerAdapter) mViewpagerChild.getAdapter();
if (adapter == null) {
/* In this case I use getChildFragmentManager() because
it's inside the last fragment of the ViewPager Father*/
adapter = new DefaultStateViewPagerAdapter(getChildFragmentManager(), fragments);
mViewpagerChild.setAdapter(adapter);
} else {
adapter.setFragments(fragments); // Into this method I do notifyDataSetChanged() already
}
}
}


Note that I try to use the same adapter's instance to set the fragments and then notify the changes (
notifyDataSetChanged()
is inside the method). If I don't get the adapter's instance, I create a new one and set it to the
ViewPager
Child.

The problem happens, for example, when I set the
ViewPager
Child with 2 fragments and after a while I need to set it with 1 fragment. The
ViewPager
shows just 1 fragment inside it, but the second one is still attached and isn't destroyed. I know it because I did a test calling
getChildFragmentManager().getFragments()
, and I could see the fragment which was supposed to be destroyed is still there.

You may say it isn't actually a problem, since the
Garbage Collector
can remove the unused
Fragment
. However, if in some moment for example, I try to set 2 new fragments again into the
ViewPager
Child, it uses that unused
Fragment
instance instead of creating a new one and unfortunately it also uses its same object, and not the right new one.

This is my
DefaultStateViewPagerAdapter
code:

public class DefaultStateViewPagerAdapter extends FragmentStatePagerAdapter {

private ArrayList<Fragment> mFragments;
private ArrayList<String> mFragmentTitles;

public DefaultStateViewPagerAdapter(FragmentManager fm) {
super(fm);
this.mFragments = new ArrayList<>();
this.mFragmentTitles = new ArrayList<>();
}

public DefaultStateViewPagerAdapter(FragmentManager fm, List<Fragment> fragments) {
super(fm);
this.mFragments = (ArrayList<Fragment>) fragments;
}

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

@Override
public int getItemPosition(Object object) {
int index = mFragments.indexOf(object);
if (index < 0) {
index = POSITION_NONE;
}
return index;
}

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

@Override
public CharSequence getPageTitle(int position) {
return mFragmentTitles.get(position);
}

public void clearFragments() {
mFragments.clear();
mFragmentTitles.clear();
}

public void setFragments(List<Fragment> fragments) {
mFragments = (ArrayList<Fragment>) fragments;
notifyDataSetChanged();
}

public void addFragment(Fragment fragment, String title) {
mFragments.add(fragment);
mFragmentTitles.add(title);
}

}


I already tried to override
saveState
in order to avoid the
ViewPager
uses the old fragment´s instance, like:

@Override
public Parcelable saveState() {
return null;
}


It worked, and the
ViewPager
no longer uses the old reference, but the unused
Fragment
is still attached, and it causes memory leak.

I don't know why the
ViewPager
doesn't destroy its fragments even after I set a new adapter. Has anyone ever had this issue?

Answer Source

I found the solution.

The solution is very simple. I just had to set manually null to the Child's adapter. With this, the ViewPager is forced to destroy every fragment.

So into the onDestroyView of Fragment's father I added:

@Override
public void onDestroyView() {
    super.onDestroyView();
    mViewpagerChild.removeOnPageChangeListener(mOnPaymentMethodsPageChangeListener);
    mViewpagerChild.setAdapter(null); // <-- This is what I added
}