Demystifying Android’s commitAllowingStateLoss()

Many, if not all Android developers have came across the following dreaded exception:

java.lang.IllegalStateException: Can not perform this action after onSaveInstanceState with DialogFragment

This happens if you call FragmentTransaction.commit() after Activity.onStop(). The problems surface usually when you are executing transactions in callbacks (e.g. API requests) which can get delivered too late.

There are several ways developers address this issue — either check if the Activity/Fragment is resumed and don’t execute the transaction in that case (which is usually wrong — the user could have just locked the screen), or they delay the code until the user returns (works, but introduces another state into the code), or they use FragmentTransaction.commitAllowingStateLoss()

What happens under the hood

It’s really simple, it actually executes the same code as commit(), so here is the simplified code:

public int commitAllowingStateLoss() {
return commitInternal(true);
}
public int commit() {
return commitInternal(false);
}
int commitInternal(boolean allowStateLoss) {
//…
if (!allowStateLoss) {
checkStateLoss();
}
}

The only difference is that it will throw an IllegalStateException() if the method was called after the state was saved.

So what are the drawbacks here?

The javadoc states:

Like commit() but allows the commit to be executed after an activity's state is saved. This is dangerous because the commit can be lost if the activity needs to later be restored from its state, so this should only be used for cases where it is okay for the UI state to change unexpectedly on the user.

What does it actually mean? Here are all the possible cases that may happen:

  • You call commitAllowingStateLoss() while the application is resumed.

In this case it will behave exactly as if you have called commit(). The fragment will be on its’ place if the user returns back after locking or rotating the screen. Its’ state will be preserved in the Bundle and the Fragment will be recreated even if the process is killed.

  • You call commitAllowingStateLoss() after the application onStop() was called and the user returns back to the app before the process is killed.

The Fragment will be displayed, even if the call was made after onStop(). The fragment will be present if you rotate the screen or return back after locking the screen. So far — this behaves the same as the regular commit().

  • You call commitAllowingStateLoss() after the application onStop() was called, the application process is killed by the system (low memory) while in background and the user returns back to your application.

The fragment transaction is lost in this case — Android will restore your application state, activities and fragments, but it has no track of your Fragment transaction.

So to recap — if the Fragment transaction is not critical (e.g. Dialog which can be triggered again by the user) and you can accept the fact the Fragment will not be visible if it was displayed after onStop() and the process was killed, then commitAllowingStateLoss() is a viable solution.

Also some applications logout the user or return back to a home screen after the process was killed — in that case using commitAllowingStateLoss() for all transactions (happening from async callbacks) is a good choice. Some other apps “re-initialise” the state of the activity after a process kill, in that case using commitAllowingStateLoss() will also do no harm.

Display a DialogFragment with commitAllowingStateLoss()

The DialogFragment.show(FragmentManager manager, 
 String tag)
doesn’t have an option to show the dialog while allowing state-loss. It’s a bit strange and inconsistent, because there is a DialogFragment.dismissAllowingStateLoss().

But you can always commit the DialogFragment transaction manually, or create a helper method like this:

public static void showDialogAllowingStateLoss(FragmentManager fragmentManager, DialogFragment dialogFragment, String tag) {

FragmentTransaction ft = fragmentManager.beginTransaction();
ft.add(dialogFragment, tag);
ft.commitAllowingStateLoss();
}

commitNowAllowingStateLoss()

Added in Support Library v24, it has the same functionality as commitAllowingStateLoss() but it also executes the transaction synchronously (so on the next line of code, you can already find the fragment on its’ place).

This is a more granular alternative to calling FragmentManager.executePendingTransactions() which not only executes your current transactions but also any other pending transactions.

Also, same as commitNow(), this can’t be used for transactions with addToBackStack(String). Which limits the use considerably.

There seems to be a bug in the implementation though… https://code.google.com/p/android/issues/detail?id=218912

No silver-bullet solution

The commitAllowingStateLoss() method should not be misused for all Fragment transactions. Test your application (Developer options -> Don’t keep Activities, or kill process while in background) and use only for calls that are called from asynchronous operations. Don’t use for Fragment transactions that are critical for the application functionality.

You should generally try to stay away from executing Fragment transactions inside callbacks. I have seen many actual problems being hidden by using commitAllowingStateLoss() or just adding isAdded() / isResumed() code on every possible location. Always first analyse why the transaction is being executed after onStop(). Did you maybe forgot to unregister a listener? Or are you handling a request after you finished the Activity?

Further reading:

http://www.androiddesignpatterns.com/2013/08/fragment-transaction-commit-state-loss.html