IllegalStateException : Cannot perform this action after onSaveInstanceState()

Spread knowledge

Many of you might have encountered this exception while working with fragments . The source for this issue can be different based on the use case but if you look under the hood this happens only because of one reason- mismanagement of Activity states. In this article we will be discussing the same. First we will explain what happens behind the scenes and what is source of this crash and then most importantly how to resolve it.

stack_trace.PNG

A very popular use case in which this issue is encountered 

Consider you are sending a network request from an activity and want to place a fragment using FragmentTransaction.commit() once the network request is successful. Suppose user presses home button before the request returns successfully, this will lead to onSaveInstanceState of the Activity being called and hence when you try to commit the fragment transaction after this the app will crash with the above error.

Under the hood

The main root cause of this exception is that fragment transactions are not allowed after the OnSaveInstanceState of the activity is called.

When is onSaveInstanceState() called?

Mobile devices are known to have limited resources so Android system can kill any activity which is not in foreground when it is short of resources. When the user navigates back to the killed activity android recreates it and after recreation it is the responsibility of the developer to ensure that his activity is in the same state. To support this Android invokes this method onSaveInstanceState just before the activity is killed. Developer is expected to save the state of his activity in this method. This shouldn’t be confused with lifecycle callbacks like onPause(),onStop() which are definitely called when the activity goes in background whereas this is called only before the activity is killed.¬† Post honeycomb Activities are considered killable only after onStop() which means onSaveInstanceState will be called before onStop.

Scenarios onSaveInstanceState() called
When the user presses Home Button Yes
When the user presses Back button No
When the user navigates from Activity A to Activity B Not called on Activity A (unless the Activity A is killed in background)
  • When you commit a Fragment(using fragmentTransaction.commit) the transaction wont happen immediately it is rather scheduled on the worker thread whenever the thread has time.
  • You wont encounter this exception if you commit a FragmentTransaction in onCreate or in response to a user click but when you try to do this after lifecycle callbacks like onPause(),onSaveInstanceState() chances are high that you will encounter an exception.
  • In simple terms this could be understood as fragment should only be committed when the activity is in foreground else you will encounter IllegalStateException.

The Solution

I hope by now you understand how important are activity states when making a fragment transaction. Now to resolve this Android itself has not come up with a concrete solution it rather advises developers to be careful while commiting fragmentTransaction outside the lifecycle of an activity. Having said that Android does provide a API to work around this issue

  1. commitAllowStateLoss
    This does the same this what commit does but even if the state is lost this wont throw an exception. So basically we are not solving the issue but rather hiding it. Android developer docs describe this as dangerous since it might lead to unexpected UI behavior where the fragment commit is lost if the activity is restored again. But this will be handy if the UI behavior doesn’t matter.
  2. Custom Solution
    I personally believe this is the best way to avoid this exception. So far we have understood that IllegalStateException is encountered when we try to commit a fragment after the Activity state is lost- with this method we just delay the transaction until the state is restored. This is how it goes

Example :

  1. To resolve this exception we will first make sure we encounter it everytime we commit a fragment. On purpose we are calling commit after onSaveInstanceState so that IllegalStateException is encountered
public class MainActivity extends AppCompatActivity {

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

    }

    public void onSaveInstanceState(Bundle bundle) {
        super.onSaveInstanceState(bundle);
        commitFragment();

    }

    private void commitFragment() {

        MyFragment myFragment = new MyFragment();
        FragmentManager fragmentManager = getFragmentManager();
        FragmentTransaction fragmentTransaction = fragmentManager.beginTransaction();
        fragmentTransaction.add(R.id.frame, myFragment);
        fragmentTransaction.commit();
    }
}

 

  • Now when we run the above code app crashes every time we press the home button with IllegalStateException
  • Coming to the solution, declare two boolean variables initialized by default to false like below
public class MainActivity extends AppCompatActivity {

    //Boolean variable to mark if the transaction is safe
    private boolean isTransactionSafe;

    //Boolean variable to mark if there is any transaction pending
    private boolean isTransactionPending;

 

  • Now in onPostResume() and onPause we set and unset our boolean variable isTransactionSafe. Idea is to mark trasnsaction safe only when the activity is in foreground so there is no chance of stateloss.
/*
onPostResume is called only when the activity's state is completely restored. In this we will
set our boolean variable to true. Indicating that transaction is safe now
*/
   public void onPostResume() {
    super.onPostResume();
    isTransactionSafe = true;
   }
/*
onPause is called just before the activity moves to background and also before onSaveInstanceState. In this
we will mark the transaction as unsafe
*/

   public void onPause() {
     super.onPause();
     isTransactionSafe = false;
   }

 

  • We will check for safe transaction before commiting a FragmentTransaction as shown below
private void commitFragment() {
    if (isTransactionSafe) {
        MyFragment myFragment = new MyFragment();
        FragmentManager fragmentManager = getFragmentManager();
        FragmentTransaction fragmentTransaction = fragmentManager.beginTransaction();
        fragmentTransaction.add(R.id.frame, myFragment);
        fragmentTransaction.commit();
    }
}
  • What we have done so far will save from IllegalStateException but our transactions will be lost if they are done after the activity moves to background, kind of like commitAllowStateloss(). To help with that we have isTransactionPending boolean variable
private void commitFragment() {

    if (isTransactionSafe) {
        MyFragment myFragment = new MyFragment();
        FragmentManager fragmentManager = getFragmentManager();
        FragmentTransaction fragmentTransaction = fragmentManager.beginTransaction();
        fragmentTransaction.add(R.id.frame, myFragment);
        fragmentTransaction.commit();
        isTransactionPending = false;
    } else {
/*
If any transaction is not done because the activity is in background. We set the isTransactionPending variable to true so that we can pick this up when we come back to foreground
*/
        isTransactionPending = true;
    }
}

 

  • Now we check for isTransactionPending variable everytime the activity comes to foreground
public void onPostResume() {
    super.onPostResume();
    isTransactionSafe = true;
/* Here after the activity is restored we check if there is any transaction pending from the last restoration
*/
    if (isTransactionPending) {
        commitFragment();
    }
}

 

  • With these two variables we have ensured that all our fragment transactions happen in sync with the lifecycle of the activity.
  • This solution even works if you have multiple fragments in the same activity. You will just need all your fragments to have a unique identifier and one more integer variable which will be initialized to the selected fragment. With that you can keep track of which fragment needs to be committed

Spread knowledge

2 Replies to “IllegalStateException : Cannot perform this action after onSaveInstanceState()”

Leave a Reply

Your email address will not be published. Required fields are marked *