15

Just to be clear, I have read the dozen top SO questions on "IllegalStateException: Can not perform this action after onSaveInstanceState" and I have read Alex Lockwood's blog post on the issue http://www.androiddesignpatterns.com/2013/08/fragment-transaction-commit-state-loss.html

So I'm not asking this blindly.

I have a very simple use case case that doesn't involve AsyncTask or any background processing.

I have a Fragment that contains a button. On the onClickListener for the button, I create a DialogFragment and show it.

public final class OverviewFragment extends Fragment {
    @Override
    public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
        final View view = inflater.inflate(R.layout.overview_fragment, container, false);

        startNewGameButton = (Button) view.findViewById(R.id.buttonNewGame);
        startNewGameButton.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View view) {
                final NewGameFragment dialogFrag = NewGameFragment.create(getApplication());
                dialogFrag.show(getFragmentManager(), NewGameFragment.FRAGMENT_TAG);
            }
        });
}

[NewGameFragment]

public final class NewGameFragment extends DialogFragment {

    public static final String FRAGMENT_TAG = "NewGameFragment";

    private static final String MESSAGE = "message";

    public static NewGameFragment create(Context context) {
        final AppsPreferences prefs = new AppPreferences(context);
        final int startOption = prefs.getGameStartOption();

        final Bundle bundle = new Bundle();
        bundle.putString(MESSAGE, getMessage(context, startOption));

        final NewGameFragment fragment = new NewGameFragment();
        fragment.setArguments(bundle);
        return fragment;
    }

    @Override
    public final Dialog onCreateDialog(Bundle savedInstanceState) {
        final String message = getArguments().getString(MESSAGE);

        final AlertDialog.Builder builder = new AlertDialog.Builder(getActivity())
            .setTitle(R.string.progress_startGame_title)
            .setMessage(message);

        builder.setPositiveButton(R.string.progress_startGame_raceButton, new DialogInterface.OnClickListener() {
            @Override
            public void onClick(DialogInterface dialog, int which) {
                new RaceAction().execute();
            }
        });
        builder.setNegativeButton(R.string.progress_startGame_eventButton, new DialogInterface.OnClickListener() {
            @Override
            public void onClick(DialogInterface dialog, int which) {
                new EventAction().execute();
            }
        });

        final Dialog dialog = builder.create();
        dialog.setCanceledOnTouchOutside(false); // Whether clicking outside the dialog closes the dialog.
        return dialog;
    }
  }

[Stacktrace]

java.lang.IllegalStateException: Can not perform this action after onSaveInstanceState
at android.support.v4.app.FragmentManagerImpl.void checkStateLoss()(SourceFile:1365)
at android.support.v4.app.FragmentManagerImpl.void enqueueAction(java.lang.Runnable,boolean)(SourceFile:1383)
at android.support.v4.app.BackStackRecord.int commitInternal(boolean)(SourceFile:636)
at android.support.v4.app.BackStackRecord.int commit()(SourceFile:615)
at android.support.v4.app.DialogFragment.void show(android.support.v4.app.FragmentManager,java.lang.String)(SourceFile:138)
at au.com.xandar.thegame.overview.OverviewFragment$1.void onClick(android.view.View)(SourceFile:160)
at android.view.View.performClick(View.java:4162)
at android.view.View$PerformClick.run(View.java:17082)
at android.os.Handler.handleCallback(Handler.java:615)
at android.os.Handler.dispatchMessage(Handler.java:92)
at android.os.Looper.loop(Looper.java:137)
at android.app.ActivityThread.main(ActivityThread.java:4867)
at java.lang.reflect.Method.invokeNative(Native Method)
at java.lang.reflect.Method.invoke(Method.java:511)
at com.android.internal.os.ZygoteInit$MethodAndArgsCaller.run(ZygoteInit.java:1007)
at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:774)
at dalvik.system.NativeStart.main(Native Method)

NB the Fragment and DialogFragment both come from support-v4:21.0.0

I am seeing this on a range of devices running 4.4. But at least one instance has occurred on a Nexus 7 running 5.0.

I have not been able to replicate this myself. Not even by introducing an artificial delay into the onClick and attempting to rotate, back, home the app.

So since the FragmentTransaction (for DialogFrag#show()) is being created and committed on the UI Thread directly from onClick(), how can the Fragment already have proceeded past onSaveInstanceState()?

Does it mean that I need to check the state of the Activity Lifecycle at the start of every user input? - very bad (the Lifecycle is meant to be handling that for me. I shouldn't be receiving user input if the Activity is already past onPause())

Does it mean I need to check the state of the Activity Lifecycle prior to every statement during execution of user input? - broken bad !!

What can I do to stop this occurring?

Further info:

After running in the wild for several days I can categorically say that getChildFragmentManager() is not the solution.

Failure occurs for the following Android versions:

  • 4.4.2 90%
  • 4.4.4 5%
  • 5.0 5%
William
  • 20,150
  • 8
  • 49
  • 91
  • post NewGameFragment class also – SweetWisher ツ Dec 06 '14 at 08:53
  • Try to move it to the `onActivityCreated` – Blaze Tama Dec 06 '14 at 08:56
  • @BlazeTama move what? Showing the dialog? That defeats the purpose of having it shown on the user clicking the button. – William Dec 06 '14 at 09:07
  • @William sorry, i mean try to move `startNewGameButton.setOnClickListener` to the `onActivityCreated`, the `onClick` will still working fine – Blaze Tama Dec 06 '14 at 09:08
  • @SweetWisherツ DialogFrag code added. – William Dec 06 '14 at 09:23
  • NewGameFragment extends `DialogFragment` why *Fragment* ? – SweetWisher ツ Dec 06 '14 at 09:23
  • @SweetWisherツ copy and paste error on my part (into this issue) - see update – William Dec 06 '14 at 09:25
  • try with `dialogFrag.show(getSupportFragmentManager(), NewGameFragment.FRAGMENT_TAG);` – SweetWisher ツ Dec 06 '14 at 09:26
  • @BlazeTama I can't see how that will help. There is no Activity context required or used in the construction of the OnClickHandler. And in can't be invoked before then anyway. – William Dec 06 '14 at 09:27
  • have you initialized `startNewGameButton`?? – SweetWisher ツ Dec 06 '14 at 09:28
  • @SweetWisherツ there is no `getSupportFragmentManager()` method on a Fragment, only on an Activity. From a support Fragment `getFragmentManager()` already returns the SupportFragmentManager. – William Dec 06 '14 at 09:29
  • Let us [continue this discussion in chat](http://chat.stackoverflow.com/rooms/66311/discussion-between-william-and-sweetwisher-). – William Dec 06 '14 at 09:30
  • have you tried to pass `getChildFragmentManager()` instead of `getSupportFragmentManager()` – mmlooloo Dec 06 '14 at 09:52
  • @mmlooloo No, I didn't even know this method existed. But it sound *really* promising. Thanks – William Dec 06 '14 at 09:55
  • Haven't looked too deep into your code yet, because I'm on my way out the door... but is there any reason why you are using `getApplication()` instead of `getActivity()` in your `OverviewFragment`? – Alex Lockwood Dec 06 '14 at 14:42
  • @AlexLockwood, just a general pattern that I use to limit references to an Activity that may go stale. In this case it's not all that important as the Context is used immediately to resolve Prefs and Strings and then discarded. I could swap in getActivity() but I cannot see how that would have any effect. – William Dec 06 '14 at 21:10
  • Did you find a solution to this issue? – Michael May 31 '17 at 20:21

1 Answers1

4

OK, as far as I can tell this is a bug in the AOSP as I have also seen an instance of this from pure Android stack (ie nothing of my code at all).

So it looks like there is a threading issue in the Activity/Fragment lifecycle in which a UI thread can get priority to respond to a button click AFTER the Activity/Fragment has already saved it's state.

My work around which has been 100% successful so far is to catch the IllegalStateException and schedule the dialog show for the next time the Activity/Fragment becomes active using a PauseHandler https://stackoverflow.com/a/25322330/493682

Community
  • 1
  • 1
William
  • 20,150
  • 8
  • 49
  • 91
  • Hi, encountered the same issue. Did you happen to log it in AOSP issue tracker? – hidro Nov 16 '15 at 03:41
  • Thanks, I checked a bunch of similar AOSP bugs earlier. Surprisingly all of them are marked as obsolete. – hidro Nov 16 '15 at 05:02