Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

On showing dialog I get "Can not perform this action after onSaveInstanceState"

This is common issue. We solved this issue by overriding show() and handling exception in DialogFragment extended class

public class CustomDialogFragment extends DialogFragment {

    @Override
    public void show(FragmentManager manager, String tag) {
        try {
            FragmentTransaction ft = manager.beginTransaction();
            ft.add(this, tag);
            ft.commit();
        } catch (IllegalStateException e) {
            Log.d("ABSDIALOGFRAG", "Exception", e);
        }
    }
}

Note that applying this method will not alter the internal fields of the DialogFragment.class:

boolean mDismissed;
boolean mShownByMe;

This may lead to unexpected results in some cases. Better use commitAllowingStateLoss() instead of commit()


That mean you commit() (show() in case of DialogFragment) fragment after onSaveInstanceState().

Android will save your fragment state at onSaveInstanceState(). So, if you commit() fragment after onSaveInstanceState() fragment state will be lost.

As a result, if Activity get killed and recreate later the fragment will not add to activity which is bad user experience. That's why Android does not allow state loss at all costs.

The easy solution is to check whether state already saved.

boolean mIsStateAlreadySaved = false;
boolean mPendingShowDialog = false;

@Override
public void onResumeFragments(){
    super.onResumeFragments();
    mIsStateAlreadySaved = false;
    if(mPendingShowDialog){
        mPendingShowDialog = false;
        showSnoozeDialog();
    }
}

@Override
public void onPause() {
    super.onPause();
    mIsStateAlreadySaved = true;
}

private void showSnoozeDialog() {
    if(mIsStateAlreadySaved){
        mPendingShowDialog = true;
    }else{
        FragmentManager fm = getSupportFragmentManager();
        SnoozeDialog snoozeDialog = new SnoozeDialog();
        snoozeDialog.show(fm, "snooze_dialog");
    }
}

Note: onResumeFragments() will call when fragments resumed.


private void showSnoozeDialog() {
    FragmentManager fm = getSupportFragmentManager();
    SnoozeDialog snoozeDialog = new SnoozeDialog();
    // snoozeDialog.show(fm, "snooze_dialog");
    FragmentTransaction ft = getSupportFragmentManager().beginTransaction();
    ft.add(snoozeDialog, "snooze_dialog");
    ft.commitAllowingStateLoss();
}

ref: link


Using the new lifecycle scopes of Activity-KTX its as simple as the following code sample:

lifecycleScope.launchWhenResumed {
   showErrorDialog(...)
}

This method can directly be called after onStop() and will successfully show the dialog once onResume() has been called upon returning.


After few days I want share my solution how I've fixed it, to show DialogFragment you should to override show() method of it and call commitAllowingStateLoss() on Transaction object. Here is example in Kotlin:

override fun show(manager: FragmentManager?, tag: String?) {
        try {
            val ft = manager?.beginTransaction()
            ft?.add(this, tag)
            ft?.commitAllowingStateLoss()
        } catch (ignored: IllegalStateException) {

        }

    }

If the dialog is not really important (it is okay to not-show it when the app closed/is no longer in view), use:

boolean running = false;

@Override
public void onStart() {
    running = true;
    super.onStart();
}

@Override
public void onStop() {
    running = false;
    super.onStop();
}

And open your dialog(fragment) only when we're running:

if (running) {
    yourDialog.show(...);
}

EDIT, PROBABLY BETTER SOLUTION:

Where onSaveInstanceState is called in the lifecycle is unpredictable, I think a better solution is to check on isSavedInstanceStateDone() like this:

/**
 * True if SavedInstanceState was done, and activity was not restarted or resumed yet.
 */
private boolean savedInstanceStateDone;

@Override
protected void onResume() {
    super.onResume();

    savedInstanceStateDone = false;
}

@Override
protected void onStart() {
    super.onStart();

    savedInstanceStateDone = false;
}

protected void onSaveInstanceState(Bundle outState) {
    super.onSaveInstanceState(outState);
    savedInstanceStateDone = true;
}


/**
 * Returns true if SavedInstanceState was done, and activity was not restarted or resumed yet.
 */
public boolean isSavedInstanceStateDone() {
    return savedInstanceStateDone;
}

I have run in to this problem for years.
The Internets are littered with scores (hundreds? thousands?) of discussions about this, and confusion and disinformation in them seems aplenty.
To make the situation worse, and in the spirit of the xkcd "14 standards" comic, I am throwing in my answer in to the ring.
xkcd 14 standards

The cancelPendingInputEvents(), commitAllowingStateLoss(), catch (IllegalStateException e), and similar solutions all seem atrocious.

Hopefully the following easily shows how to reproduce and fix the problem:

private static final Handler sHandler = new Handler();
private boolean mIsAfterOnSaveInstanceState = true;

@Override
protected void onSaveInstanceState(Bundle outState)
{
    super.onSaveInstanceState(outState);
    mIsAfterOnSaveInstanceState = true; // <- To repro, comment out this line
}

@Override
protected void onPostResume()
{
    super.onPostResume();
    mIsAfterOnSaveInstanceState = false;
}

@Override
protected void onResume()
{
    super.onResume();
    sHandler.removeCallbacks(test);
}

@Override
protected void onPause()
{
    super.onPause();
    sHandler.postDelayed(test, 5000);
}

Runnable test = new Runnable()
{
    @Override
    public void run()
    {
        if (mIsAfterOnSaveInstanceState)
        {
            // TODO: Consider saving state so that during or after onPostResume a dialog can be shown with the latest text
            return;
        }

        FragmentManager fm = getSupportFragmentManager();
        DialogFragment dialogFragment = (DialogFragment) fm.findFragmentByTag("foo");
        if (dialogFragment != null)
        {
            dialogFragment.dismiss();
        }

        dialogFragment = GenericPromptSingleButtonDialogFragment.newInstance("title", "message", "button");
        dialogFragment.show(fm, "foo");

        sHandler.postDelayed(test, 5000);
    }
};