Fragment in ViewPager not restored after popBackStack

AndroidAndroid FragmentsAndroid Viewpager

Android Problem Overview


##Problem A Fragment is not reattached to its hosting ViewPager after returning from another fragment.

##Situation One Activity hosting a Fragment whose layout holds a ViewPager (PageListFragment in the example below). The ViewPager is populated by a FragmentStateViewPagerAdapter. The single Fragments hosted inside the pager (PageFragment in the example below) can open sub page lists, containing a new set of pages.

##Behaviour All works fine as long as the back button is not pressed. As soon as the user closes one of the sub PageLists the previous List is recreated, but without the Page that was displayed previously. Swiping through the other pages on the parent PageList still works.

##Code A sample application can be found on github:

###Activity

public class MainActivity extends FragmentActivity {

private static final String CURRENT_FRAGMENT = MainActivity.class.getCanonicalName() + ".CURRENT_FRAGMENT";

public static final String ARG_PARENTS = "Parents";

public void goInto(String mHostingLevel, String mPosition) {
	Fragment hostingFragment = newHostingFragment(mHostingLevel, mPosition);
	addFragment(hostingFragment);
}

@Override
protected void onCreate(Bundle savedInstanceState) {
	super.onCreate(savedInstanceState);
	setContentView(R.layout.activity_main);
	addBaseFragment();
}

private void addBaseFragment() {
	Fragment hostingFragment = newHostingFragment("", "");
	addFragment(hostingFragment);
}

private Fragment newHostingFragment(String mHostingLevel, String oldPosition) {
	Fragment hostingFragment = new PageListFragment();
	Bundle args = new Bundle();
	args.putString(ARG_PARENTS, mHostingLevel + oldPosition +" > ");
	hostingFragment.setArguments(args);
	return hostingFragment;
}

private void addFragment(Fragment hostingFragment) {
	FragmentTransaction transaction = getSupportFragmentManager().beginTransaction();
	transaction.replace(R.id.fragmentSpace, hostingFragment, CURRENT_FRAGMENT);
	transaction.addToBackStack(null);
	transaction.commit();
}

}

###PageListFragment public class PageListFragment extends Fragment {

private String mParentString;

public PageListFragment() {
	// Required empty public constructor
}

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

@Override
public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
	// Inflate the layout for this fragment
	return inflater.inflate(R.layout.fragment_hosting, container, false);
}

@Override
public void onResume() {
	mParentString = getArguments().getString(MainActivity.ARG_PARENTS);
	ViewPager viewPager = (ViewPager) getView().findViewById(R.id.viewPager);
	viewPager.setAdapter(new SimpleFragmentStatePagerAdapter(getFragmentManager(),mParentString));
	super.onResume();
}

private static class SimpleFragmentStatePagerAdapter extends FragmentStatePagerAdapter {

	private String mHostingLevel;

	public SimpleFragmentStatePagerAdapter(FragmentManager fm, String hostingLevel) {
		super(fm);
		this.mHostingLevel = hostingLevel;
	}

	@Override
	public android.support.v4.app.Fragment getItem(int position) {
		PageFragment pageFragment = new PageFragment();
		Bundle args = new Bundle();
		args.putString(MainActivity.ARG_PARENTS, mHostingLevel);
		args.putInt(PageFragment.ARG_POSITION, position);
		pageFragment.setArguments(args);
		return pageFragment;
	}

	@Override
	public int getCount() {
		return 5;
	}
}
}

###PageFragment public class PageFragment extends Fragment {

public static final String ARG_POSITION = "Position";

private String mHostingLevel;
private int mPosition;

public PageFragment() {
	// Required empty public constructor
}

@Override
public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
	View contentView = inflater.inflate(R.layout.fragment_page, container, false);
	setupTextView(contentView);
	setupButton(contentView);
	return contentView;
}

private void setupTextView(View contentView) {
	mPosition = getArguments().getInt(ARG_POSITION);
	mHostingLevel = getArguments().getString(MainActivity.ARG_PARENTS);
	TextView text = (TextView) contentView.findViewById(R.id.textView);
	text.setText("Parent Fragments " + mHostingLevel + " \n\nCurrent Fragment "+ mPosition);
}

private void setupButton(View contentView) {
	Button button = (Button) contentView.findViewById(R.id.button);
	button.setOnClickListener(new View.OnClickListener() {
		@Override
		public void onClick(View v) {
			openNewLevel();
		}
	});
}

protected void openNewLevel() {
	MainActivity activity = (MainActivity) getActivity();
	activity.goInto(mHostingLevel, Integer.toString(mPosition));
}

}

Android Solutions


Solution 1 - Android

After a lengthy investigation it turns out to be a problem with the fragment manager.

When using a construct like the one above the fragment transaction to reattach the fragment to the page list is silently discarded. It is basically the same problem that causes a

java.lang.IllegalStateException: Recursive entry to executePendingTransactions 

when trying to alter the fragments inside the FragmentPager.

The same solution, as for problems with this error, is also applicable here. When constructing the FragmentStatePagerAdapter supply the correct child fragment manager.

Instead of

    viewPager.setAdapter(new SimpleFragmentStatePagerAdapter(getFragmentManager(),mParentString));

do

    viewPager.setAdapter(new SimpleFragmentStatePagerAdapter(getChildFragmentManager(),mParentString));

See also: github

Solution 2 - Android

What Paul has failed to mention is, if you use getChildFragmentManager, then you will suffer the "blank screen on back pressed" issue.

Solution 3 - Android

The hierarchy in my case was:

MainActivity->MainFragment->TabLayout+ViewPager->AccountsFragment+SavingsFragment+InvestmentsFragment etc.

The problem I had was that I couldn't use childFragmentManagerfor the reason that a click on the item Account view (who resides inside one of the Fragments of the ViewPager) needed to replace MainFragment i.e. the entire screen.

enter image description here

Using MainFragments host Fragment i.e. passing getFragmentManager() enabled the replacing, BUT when popping the back-stack, I ended up with this screen:

enter image description here

This was apparent also by looking at the layout inspector where the ViewPager is empty.

enter image description here

Apparently looking at the restored Fragments you would notice that their View is restored but will not match the hierarchy of the popped state. In order to make the minimum impact and not force a re-creation of the Fragments I re-wrote FragmentStatePagerAdapter with the following changes:

I copied the entire code of FragmentStatePagerAdapter and changed

@NonNull
@Override
public Object instantiateItem(@NonNull 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;
        }
    }
...
}

with

@NonNull
@Override
public Object instantiateItem(@NonNull 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) {
            if (mCurTransaction == null) {
                mCurTransaction = mFragmentManager.beginTransaction();
            }

            mCurTransaction.detach(f);
            mCurTransaction.attach(f);

            return f;
        }
    }
...
}

This way I am effectively making sure that that the restored Fragments are re-attached to the ViewPager.

Solution 4 - Android

Delete all page fragments, enabling them to be re-added later

The page fragments are not attached when you return to the viewpager screen as the FragmentStatePagerAdapter is not re-connecting them. As a work-around, delete all the fragments in the viewpager after popbackstack() is called, which will allow them to be re-added by your initial code.

[This example is written in Kotlin]

//Clear all fragments from the adapter before they are re-added.
for (i: Int in 0 until adapter.count) {
    val item = childFragmentManager.findFragmentByTag("f$i")
    if (item != null) {
        adapter.destroyItem(container!!, i, item)
    }
}

Attributions

All content for this solution is sourced from the original question on Stackoverflow.

The content on this page is licensed under the Attribution-ShareAlike 4.0 International (CC BY-SA 4.0) license.

Content TypeOriginal AuthorOriginal Content on Stackoverflow
QuestionPaulView Question on Stackoverflow
Solution 1 - AndroidPaulView Answer on Stackoverflow
Solution 2 - AndroidQylinView Answer on Stackoverflow
Solution 3 - AndroidCyrus Bakhtiari-HaftlangView Answer on Stackoverflow
Solution 4 - AndroidJames PorterView Answer on Stackoverflow