44

I sometimes see the following stacktrace for a commit that can happen when the user isn't looking at the activity (after state's been saved):

java.lang.IllegalStateException: Can not perform this action after onSaveInstanceState
    at android.support.v4.app.FragmentManagerImpl.checkStateLoss(FragmentManager.java:1327)
    at android.support.v4.app.FragmentManagerImpl.enqueueAction(FragmentManager.java:1338)
    at android.support.v4.app.BackStackRecord.commitInternal(BackStackRecord.java:595)
    at android.support.v4.app.BackStackRecord.commit(BackStackRecord.java:574)

Looking at the Android source, this makes total sense:

private void checkStateLoss() {
        if (mStateSaved) {
            throw new IllegalStateException(
                    "Can not perform this action after onSaveInstanceState");
        }
        if (mNoTransactionsBecause != null) {
            throw new IllegalStateException(
                    "Can not perform this action inside of " + mNoTransactionsBecause);
        }
 }

Now, I wonder if there is any way (besides storing a class variable in on(Save/Restore)InstanceState) to check if a fragment is going to be committed in an undesirable state, this way I can store the transaction for later and make the commit at the appropriate time.

azizbekian
  • 60,783
  • 13
  • 169
  • 249
hwrdprkns
  • 7,525
  • 12
  • 48
  • 69

8 Answers8

48

Starting from support library version 26.0.0 Beta 1 a new API is available in FragmentManager and Fragment classes:

FragmentManager and Fragment have an isStateSaved() method to allow querying whether or not a transaction will be allowed without state loss. This is especially useful to check when handling an onClick() event before executing any transaction.

From docs of android.support.v4.app.FragmentManager#isStateSaved():

Returns true if the FragmentManager's state has already been saved by its host. Any operations that would change saved state should not be performed if this method returns true. For example, any popBackStack() method, such as popBackStackImmediate() or any FragmentTransaction using commit() instead of commitAllowingStateLoss() will change the state and will result in an error.

This API will ship with framework's android.app.FragmentManager starting from Android O.

azizbekian
  • 60,783
  • 13
  • 169
  • 249
21

Since you did not attach any example code, all i can guess is that you're using "wrong" method when committing transaction.

so, instead of using FragmentTransaction.commit(), you should use FragmentTransaction.commitAllowingStateLoss().

Also, there are reports and workarounds about this issue (or rather change in API behavior) in this google blog post.

Tomo
  • 6,847
  • 1
  • 22
  • 32
13

It is unluckily Android Fragment does not provide an API check if it is fine to commit transaction.

But we can add a boolean field in the attached activity helping us checking. Please ref the following code.

public class GlobalBaseActivity extends FragmentActivity {

    private boolean mAllowCommit;

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

        mAllowCommit = true;
    }

    @Override
    protected void onSaveInstanceState(Bundle outState) {
        mAllowCommit = false;
        super.onSaveInstanceState(outState);
    }

    @Override
    protected void onResumeFragments() {
        mAllowCommit = true;
        super.onResumeFragments();
    }

    public boolean allowFragmentCommit() {
        return mAllowCommit;
    } 

    public void callbackOnEvent() {
        if (allowFragmentCommit()){
            getFragmentManager().beginTransaction().add(new YourFragment(), TAG).commit();
        }
    }
}

As for why choosing onResumeFragment() as allowing transaction indicator, ref this good blog. It explains famous IllegalStateException in detail.

peacepassion
  • 1,918
  • 2
  • 15
  • 19
  • 1
    I had this idea in mind (yes I did read that blog before opening this question). This is the way to go. `commitAllowingStateLoss()` is not a very decent way to fix the issue. – Sufian Jul 01 '15 at 11:26
  • `unluckily Android Fragment does not provide an API check if it is fine to commit transaction` Luckily, they do know, which is mentioned in [this](https://stackoverflow.com/a/44064149/1083957) answer. – azizbekian Feb 23 '18 at 09:05
0

RuntimeExceptions (and IllegalStateException is one of them) most often mean your code is incorrect about how it tried to achieve something. Trying to handle such exception (for example by catching it) is also wrong. Your code should never behave the way Android would throw exception like this on you.

If you use Handlers and post or send message to be handled in the future, you need to clear the queue before going out of resumed state. Also you cannot just start AsyncTask from Activity and commit transaction in onPostExecute because user could go back from your Activity. You need to cancel it and check if it was cancelled.

There are many examples like these and all are caused by "temporary" memory leaks, like I like to call them.

Basically your code is bad and without providing it, it is impossible to tell how.

MaciejGórski
  • 22,187
  • 7
  • 70
  • 94
  • 7
    This doesn't answer the question, it just criticizes it. – Eurig Jones Jun 21 '13 at 05:48
  • 1
    @Amorgos That is true. A question like that requires some criticism. A question like that has its root in not trying to understand a well designed platform and is like a doctor trying to cure the symptoms, but never the cause. If the code was there, the cause would be simple to find. – MaciejGórski Jun 21 '13 at 06:30
  • 3
    Then flag the question as that. The posters intention was to create a technical question, and expected a technical answer. He didn't show enough evidence to provide a technical answer you're right. But you're answer has just the same problems as the question! If you have a problem with a question then you flag it or comment on it. – Eurig Jones Jun 21 '13 at 06:38
0

ft.commitAllowingStateLose() is your best bet in this situation. However, as stated, there is no guarantee that your state is going to persist.

astryk
  • 1,256
  • 13
  • 20
0

You should call method commit only in these methods OnCreate, OnResumeFragments,OnPostResume. Details you can read here Fragment Transactions & Activity State Loss

Abbath
  • 1,002
  • 13
  • 30
0

Here's a Kotlin example leveraging the latest isStateSaved() FragmentManager method: https://proandroiddev.com/kotlin-extensions-to-commit-fragments-safely-de06218a1f4

worked
  • 5,762
  • 5
  • 54
  • 79
-1

See i also had this issue , i could not find a solution any where. I tried a work around .It works like this. Make the fragment transaction from a Handler instead. The handler should be invoked with a 2000 ms DELAY . Eg. on calling makeTransaction

void makeTransaction()
{
  handler.sendEmptyMessageDelayed(0,2000);
}

void actuallyMakeTransaction()
{
 // transaction code
}

Handler handler = new Handler()
{
void handleMessage(Message msg)
{
   actuallyMakeTransaction();
}
}

It solved the problem for me

DroidBoy
  • 162
  • 1
  • 11
  • 1
    Why is a 2 second delay the correct solution? Can you explain? – hwrdprkns Mar 18 '13 at 17:07
  • sorry i cannot explain because i myself am not very sure.Any way did it work for you??? – DroidBoy Mar 21 '13 at 06:55
  • This is not 1990. it's almost always a bad idea to fix symptoms by waiting. Educate yourself instead with the link Abbath provided: http://www.androiddesignpatterns.com/2013/08/fragment-transaction-commit-state-loss.html – Jan Rabe May 17 '16 at 08:00