5

I was trying to figure out why:

getSupportFragmentManager().beginTransaction().commit();

fails, with

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

in a very basic FragmentActivity class.

So here was my use-case (this will be some pseudo-code and not a complete example, sorry): I had one FragmentActivity with an internal AsyncTask class. Roughly something like this:

public class HelloWorld extends FragmentActivity {
    showFragment(Fragment fragment, String name) {
        getSupportFragmentManager().beginTransaction().replace(R.id.fragmentContainer, fragment, name).commit();
    }

    private class SlowFragmentShow extends AsyncTask<Context, String, Void> {
        protected Void doInBackground(Context... contexts) {
            try {
                Thread.sleep(10000);
            } catch (InterruptedException e) {
                /* meh */
            }
        }

        protected void onPostExecute(Void nothing) {
            showFragment(new MyFragment(), "myFragment");
        }
    }
}

Or basically, 10 seconds after starting the app, it would show another fragment. Sounds simple, right? This seemed to work well too, until I decided to rotate the phone. When I did that, the app would crash upon calling "getSupportFragmentManager()..." with the "Can not perform this action...".

CommonsWare
  • 986,068
  • 189
  • 2,389
  • 2,491
Vidar Wahlberg
  • 4,366
  • 2
  • 13
  • 10

2 Answers2

10

After lots of debugging, it turned out that when SlowFragmentShow.onPostExecute() was called, which called showFragment(), which in turn called getSupportFragmentManager(), I received a FragmentManager that was in an IllegalState (so arguably the exception I got was correct). I'm still not sure why getSupportFragmentManager() would ever return an object in such a limbo state, but it did, and I needed to somehow get access to the "correct" FragmentManager. So to cut to the chase, I stored the FragmentManager as a static variable in my HelloWorld FragmentActivity, which I updated when HelloWorld.onStart() was called:

public class HelloWorld extends FragmentActivity {
    private static FragmentManager fragmentManager;

    public void onStart() {
        fragmentManager = getSupportFragmentManager();
        /* more code here */
    }

    showFragment(Fragment fragment, String name) {
        fragmentManager.beginTransaction().replace(R.id.fragmentContainer, fragment, name).commit();
    }

    private class SlowFragmentShow extends AsyncTask<Context, String, Void> {
        protected Void doInBackground(Context... contexts) {
            try {
                Thread.sleep(10000);
            } catch (InterruptedException e) {
                /* meh */
            }
        }

        protected void onPostExecute(Void nothing) {
            showFragment(new MyFragment(), "myFragment");
        }
    }
}

And well, that pretty much fixed it. Now I could rotate the phone to my hearts desire, the fragment would still be shown when the AsyncTask was done.

In retrospect, it really seems a bit "oh, of course!", but the design decisions behind Android feels quite "alien" and unusual. I seem to end up with try-catch(Exception) around pretty much half the code just to prevent it from crashing on a non-fatal error (such as failing to update a text field), and a lot of static variables that needs to be updated upon onStart() because that seems like the only sane way you can reference Android objects without them being in an IllegalState.

Siddharth
  • 9,349
  • 16
  • 86
  • 148
Vidar Wahlberg
  • 4,366
  • 2
  • 13
  • 10
  • "a lot of static variables that needs to be updated upon onStart()" -- I would recommend that you ask a fresh StackOverflow question, with examples of this, so people can help you with better tactics. – CommonsWare Jun 14 '13 at 22:52
  • If you have a better tactic for making an AsyncTask access the (Support)FragmentManager without the static variable I've resorted to, then please, enlighten me. – Vidar Wahlberg Jun 14 '13 at 23:04
  • 3
    Call `setRetainInstance(true)` during creation of your fragment. Then call `getSupportFragmentManager()` in `onPostExecute()` to obtain the `SupportFragmentManager`. However, in the English language, "a lot" implies more than one case, which is why I suggested that you ask a fresh StackOverflow question, with examples of this, so people can help you with better tactics. – CommonsWare Jun 14 '13 at 23:08
  • Where would I call setRetainInstance(true)? There's no such method for FragmentActivity. – Vidar Wahlberg Jun 14 '13 at 23:16
  • `setRetainInstance()` is a method on `Fragment`. By "during creation", I meant during one of the early lifecycle methods. `onStart()` could work, though I've never tried it there. I usually use either `onCreateView()` (if I have one for other reasons) or `onActivityCreated()`. See https://github.com/commonsguy/cw-omnibus/tree/master/Threads/AsyncTask – CommonsWare Jun 14 '13 at 23:18
  • If HelloWorld was a Fragment, and not a FragmentActivity that would probably work. You could consider SlowFragmentShow as an async task that loads data, while HelloWorld shows something that lets the user know that the app is being initialized. I feel it wouldn't make sense to connect this async task to a Fragment rather than the activity, as it's a task for initializing the app/activity, not a fragment. As for using "a lot" of static variables: http://stackoverflow.com/questions/14083950/duplicate-id-tag-null-or-parent-id-with-another-fragment-for-com-google-androi/14695397#14695397 – Vidar Wahlberg Jun 14 '13 at 23:52
  • "I feel it wouldn't make sense to connect this async task to a Fragment" -- the model fragment pattern is alive and well for this. You are welcome to use a static `AsyncTask` inner class and `onRetainNonConfigurationInstance()` if you prefer, though this is deprecated to steer people towards retained fragments: http://stackoverflow.com/questions/3821423/background-task-progress-dialog-orientation-change-is-there-any-100-working/3821998#3821998. With respect to your other answer, never put a `View` in a static data member, as you introduce memory leaks. – CommonsWare Jun 15 '13 at 00:29
0

@VidarWahlberg your answer is good but let's say you have FragmentActivity where you run the task and you open new FragmentActivity that opens before async is over or your app is running on background(example: you clicked home). I suggest you create BaseActivity where you extend FragmentActivity and all your activities should extend it.

What is important?

In BaseActivity you need to have static FragmentManager fm;, as @VidarWahlberg mentioned in his answer, and use it in the child activity to show the DialogFragment.

What is the difference?

Instead of initializing fm = getSupportFragmentManager(); in onStart() method you need to do it in onResume(). It is needed because when you start the thread runing activity it's creating new instance of it and when starting the next activity it creates new instance of the BaseActivity where FragmenManager will be initialized and when you come back to the thread starting activity fm object be with the second activity(which is paused) instance. The application will crash because you will try showing dialog in already paused activity's view. You cannot change view of paused activity so you need to initialize whenever onResume() is called. onResume() is called also when activity starts so it's safe to initialize the FragmentManager there!

What elese we need to worry about?

You might think everything is ok but it's not. You may not realize it at first time but if your app is not at foreground(but at background) all of your activities are paused but the thread is already started and it's running. This is a problem because thread will continue and will try to show the DialogFragmernt(or let's say to change view of activity that is not on focus). So the solution is to track if app is on foreground or no. This can be done with PackageManager but it will require new permission in the manifest file(which users don't like and they may not want to install your app). The solution is to have static boolean isOnBackground = false; field that you change on every activity onResume() with isOnBackground = false; and in onPause() with isOnBackground = true;. This is not big change because BaseActivity is where we can do that. This is safe because every start of new activity creates new instance of the BaseActivity class but since the field is static it's created just once and is same for all instances. We need to check the state of isOnBackground before we start the body code of thread post method.

You may say that there can be problem because of delay between onPause() and onResume() but your app need to be designed in the way to start activity fast and not do long taking work on ui thread(need to be done on background thread). So if your app is designed good you will not have problems :)

You can use recursive call of the thread running method whenever app is not on foreground. This is safe and will not cause loop because when app's state are reseted(in need of memory) or app is stopped by user or android itself loop will be stopped because all app's instances will be freed from memory!

NOTE: Use the android support library!

Here goes the code:

BaseActivity.java

.......................
import android.support.v4.app.FragmentActivity;
import android.support.v4.app.FragmentManager;
.......................

public class BaseActivity extends FragmentActivity {

    public static FragmentManager fm;
    public static boolean isOnBackground;

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

    @Override
    protected void onResume() {
        super.onResume();
        BaseActivity.isOnBackground = false;
        BaseActivity.fm = getSupportFragmentManager();
    }

    @Override
    protected void onPause() {
        super.onPause();
        BaseActivity.isOnBackground = true;
    }
}

MainActivity.java

.....................
import android.support.v4.app.DialogFragment;
.....................

public class MainActivity extends BaseActivity {

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

        //This is just to call some second activity
        Button startNewActivity = (Button) findViewById(R.id.helloWorld);

        startNewActivity.setOnClickListener( new OnClickListener() {

            @Override
            public void onClick(View v) {
                Intent intent = new Intent(MainActivity.this,
                            SecondActivity.class);
                startActivity(intent);
            }
        });

        showEditDialog();
    }

    private void showEditDialog() {
        (new Handler()).postDelayed(new Runnable() {
            @Override
            public void run() {
                if(!BaseActivity.isOnBackground) {
                    DialogFragment myFragmentDialog = new DialogFragment();
                    myFragmentDialog.show(BaseActivity.fm,
                            "fragment_my_dialog");
                } else {
                    //this is optional
                    showEditDialog();
                }
            } 

        }, 15000);
    }
}