Demystifying Android’s commitAllowingStateLoss()

Daniel Novak
Nov 7, 2016 · 4 min read
Image for post
Image for post

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

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?

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()

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()

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

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

INLOOPX

People at INLOOPX writing about anything.

Medium is an open platform where 170 million readers come to find insightful and dynamic thinking. Here, expert and undiscovered voices alike dive into the heart of any topic and bring new ideas to the surface. Learn more

Follow the writers, publications, and topics that matter to you, and you’ll see them on your homepage and in your inbox. Explore

If you have a story to tell, knowledge to share, or a perspective to offer — welcome home. It’s easy and free to post your thinking on any topic. Write on Medium

Get the Medium app

A button that says 'Download on the App Store', and if clicked it will lead you to the iOS App store
A button that says 'Get it on, Google Play', and if clicked it will lead you to the Google Play store