A possible way to safely perform Fragment Transactions after Activity.onSaveInstanceState(…)

UPDATE (20/Nov/2017):

Christophe Beyls has pointed that commitAllowingStateLoss() achieves the same thing that this solution is trying to achieve. You can go ahead to read the post if you’re curious of the implementation.


Say you have and Android app, this app performs some network operation on a screen and if the operation is successful, you will move the user to a feedback screen. Let’s assume you’ve used Android Fragments (why won’t you) extensively in creating screens for this your app. Over time, if you’re using Firebase Crash Reporting you’ll notice that this will be your most common error.

Exception java.lang.IllegalStateException: Can not perform this action after onSaveInstanceState
android.support.v4.app.FragmentManagerImpl.checkStateLoss (FragmentManagerImpl.java:1538)
android.support.v4.app.FragmentManagerImpl.enqueueAction (FragmentManagerImpl.java:1556)
android.support.v4.app.BackStackRecord.commitInternal (BackStackRecord.java:696)
android.support.v4.app.BackStackRecord.commit (BackStackRecord.java:662)

This error occurs when you try to perform any fragment transaction (including DialogFragment.show() and DialogFragment.dismiss()) after Activity.onSaveInstanceState(…) has been called. I saw someone with same issue on StackOverflow. My solution is derived from the answers on that thread. This solution is going to cover replacing fragments using the FragmentManager. The solution is explained below, you’ll also find the full code on github.

  1. Add a flag to know when an Activity has been paused or resumed in your Activity.
//In your Activity
private boolean isRunning;
@Override
protected void onPause() {
super.onPause();
isRunning = false;
}
@Override
protected void onResume() {
super.onResume();
isRunning = true;
}

2. Create a class that will be used to hold fragment transactions that will cause the IllegalStateException to be thrown after Activity.onSaveInstanceState(…) has been called .

public abstract class DeferredFragmentTransaction {

private int contentFrameId;
private Fragment replacingFragment;

public abstract void commit();

public int getContentFrameId() {
return contentFrameId;
}

public void setContentFrameId(int contentFrameId) {
this.contentFrameId = contentFrameId;
}

public Fragment getReplacingFragment() {
return replacingFragment;
}

public void setReplacingFragment(Fragment replacingFragment) {
this.replacingFragment = replacingFragment;
}

}

3. Create a queue in your Activity that will hold all DeferredFragmentTransaction objects just in case you have more than one of them occur after Activity.onSaveInstanceState(…) has been called.

//in your Activity
Queue<DeferredFragmentTransaction> deferredFragmentTransactions = new ArrayDeque<>();

4. Create a method to replace fragments in your Activity. You can make this Activity your base Activity so that all other Activities will extend it. This method you’ve created should factor in whether the Activity is resumed or not. If the Activity is not resumed it will add the transaction to your deferredFragmentTransactions so that you will execute it whenever the user returns.

//In your Activity
public void replaceFragment(int contentFrameId, android.support.v4.app.Fragment replacingFragment) {
    if (!isRunning) {
DeferredFragmentTransaction deferredFragmentTransaction = new DeferredFragmentTransaction() {
@Override
public void commit() {
replaceFragmentInternal(getContentFrameId(), getReplacingFragment());
}
};

deferredFragmentTransaction.setContentFrameId(contentFrameId);
deferredFragmentTransaction.setReplacingFragment(replacingFragment);

deferredFragmentTransactions.add(deferredFragmentTransaction);
} else {
replaceFragmentInternal(contentFrameId, replacingFragment);
}
}

private void replaceFragmentInternal(int contentFrameId, Fragment replacingFragment) {
FragmentManager fragmentManager = getSupportFragmentManager();
fragmentManager.beginTransaction()
.replace(contentFrameId, replacingFragment)
.commit();
}

5. Finally, you’ll need to execute the deferred transaction whenever the user comes back to your app.

//In Your Activity

@Override
protected void onPostResume() {
super.onPostResume();

while (!deferredFragmentTransactions.isEmpty()) {
deferredFragmentTransactions.remove().commit();
}
}

Because of how easy it looks it feels as if there is something I’m missing. I’ve tried it on one of our apps and it has worked fine. Please inform me if you find something that can be improved. Thank you.

One clap, two clap, three clap, forty?

By clapping more or less, you can signal to us which stories really stand out.