154

Some users are reporting, if they use the quick action in the notification bar, they are getting a force close.

I show a quick action in the notification who calls the "TestDialog" class. In the TestDialog class after pressing the button "snooze", I will show the SnoozeDialog.

private View.OnClickListener btnSnoozeOnClick() {
    return new View.OnClickListener() {

        public void onClick(View v) {
            showSnoozeDialog();
        }
    };
}

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

The error is *IllegalStateException: Can not perform this action after onSaveInstanceState*.

The code line where the IllegarStateException gets fired is:

snoozeDialog.show(fm, "snooze_dialog");

The class is extending "FragmentActivity" and the "SnoozeDialog" class is extending "DialogFragment".

Here is the complete stack trace of the error:

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)
at android.support.v4.app.DialogFragment.show(DialogFragment.java:127)
at com.test.testing.TestDialog.f(TestDialog.java:538)
at com.test.testing.TestDialog.e(TestDialog.java:524)
at com.test.testing.TestDialog.d(TestDialog.java:519)
at com.test.testing.g.onClick(TestDialog.java:648)
at android.view.View.performClick(View.java:3620)
at android.view.View$PerformClick.run(View.java:14292)
at android.os.Handler.handleCallback(Handler.java:605)
at android.os.Handler.dispatchMessage(Handler.java:92)
at android.os.Looper.loop(Looper.java:137)
at android.app.ActivityThread.main(ActivityThread.java:4507)
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:790)
at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:557)
at dalvik.system.NativeStart.main(Native Method)

I can't reproduce this error, but I am getting a lot of error reports.

Can anybody help that how can I fix this error?

peterh
  • 11,875
  • 18
  • 85
  • 108
chrisonline
  • 6,949
  • 11
  • 42
  • 62
  • 2
    Did you found a solution? I have the same problem as you. I've asked here: http://stackoverflow.com/questions/15730878/can-not-perform-this-action-after-onsaveinstancestate-onclick-preference Please check my question and see the possible solution which is not working for my case. Maybe it will work for you. – rootpanthera Apr 01 '13 at 15:19
  • No solution yet :-( And your suggestion is already added to my class. – chrisonline Apr 02 '13 at 11:20
  • Check the accepted answer from here. This solved my issue: http://stackoverflow.com/questions/14177781/java-lang-illegalstateexception-can-not-perform-this-action-after-onsaveinstanc – bogdan Jul 16 '13 at 11:45
  • 4
    Is your Activity visible when this dialog is triggered? It sounds like this may be caused by your app trying to display a dialog attached to an Activity that has been paused/stopped. – Kai Sep 06 '14 at 06:27
  • http://stackoverflow.com/questions/7575921/illegalstateexception-can-not-perform-this-action-after-onsaveinstancestate-h you have tried this Im sure. – Orion Sep 09 '14 at 08:29
  • @chrisonline: Didn't you find the solution yet? I have the same problem. – Alireza Noorali Dec 08 '18 at 12:01
  • No same status. – chrisonline Dec 09 '18 at 12:17
  • 1
    I solve my same problem by changing some design to use childFragmentManager instead of supportFragmentManager. This error was occurring after orientation change when I try to show dialog. – Rahul Dec 28 '20 at 03:16

17 Answers17

80

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()

Salam El-Banna
  • 3,784
  • 1
  • 22
  • 34
Rafael
  • 6,091
  • 5
  • 54
  • 79
  • 4
    But why does this issue occur? Is it ok to ignore the error? What happens when you do? After all, when clicking, it means that the activity is live and well... Anyway, I've reported about this here because I consider this a bug: https://code.google.com/p/android/issues/detail?id=207269 – android developer Apr 19 '16 at 09:39
  • I agree with you, this kind of bugs should be fixed by Android team. – Rafael Apr 19 '16 at 09:47
  • 1
    Can you please star and/or comment there, then? – android developer Apr 19 '16 at 21:35
  • 5
    it's better to call super.show(manager, tag) inside try-catch clause. The flags owned by DialogFragment can stay safe this way – Shayan_Aryan Jan 16 '17 at 10:33
  • 24
    At this point you can call commitAllowingStateLoss() instead of commit(). The Exception would not be raised. – ARLabs Jan 30 '17 at 17:58
  • 1
    @ARLabs I imagine in this case that would be better for the answerer too since if you just catch the exception as shown here, the dialog will most definitely not be shown. Better to show the dialog if you can and it just might be gone if the state has to be restored. Also keep your app memory use low in the background so it is not likely to be destroyed. – androidguy Jan 08 '18 at 23:12
  • 9
    You just added a try-catch block, that's not a solution for the problem, you're just covering a mistake. – Dimas Mendes Oct 18 '18 at 23:21
  • 1
    @ARLabs That is basically same as try/catch it doesn't resolve anything, you just hide the actual problem – Farid Jul 11 '22 at 08:47
43

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.

PenguinDan
  • 904
  • 7
  • 15
  • 1
    Thanks! Lifecycle KTX dependency can be found here: https://developer.android.com/kotlin/ktx#lifecycle – Micer Feb 02 '21 at 14:15
  • @chrisonline Would you mind accepting this as correct solution now for future readers? No ugly workaround, let the system handle its own complication for you. BTW, this is new and was not around when OP asked the question – Farid Jul 11 '22 at 08:54
  • 4
    Thanks for sharing this. The API now has a warning for the method: `Caution: This API is not recommended to use as it can lead to wasted resources in some cases. Please, use the Lifecycle.repeatOnLifecycle API instead. This API will be removed in a future release.` – ConcernedHobbit Jul 22 '22 at 11:54
  • Isn't this only for `suspend function` invocations? – IgorGanapolsky Aug 18 '22 at 20:50
33

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.

Pongpat
  • 13,248
  • 9
  • 38
  • 51
  • 2
    What if I want to show the DialogFragment within another fragment? – android developer May 06 '15 at 10:38
  • Our solution is create activity and fragment base class and delegate onResumeFragments to fragment (we create onResumeFragments in fragment base class). It is not nice solution but it does work. If you have any better solution please let me know :) – Pongpat May 06 '15 at 12:32
  • Well, I thought that showing the dialog in the "onStart" should work fine, since the fragment is surely being shown, but I still see some crash reports about it. I was instructed to try to put it on the "onResume" instead. About alternatives, I saw this: http://twigstechtips.blogspot.co.il/2014/01/android-dealing-with-can-not-perform.html , but it's quite weird. – android developer May 06 '15 at 13:28
  • I think the reason that http://twigstechtips.blogspot.co.il/2014/01/android-dealing-with-can-not-perform.html work because it start new thread and hence all the lifecycle code i.e. onStart, onResume, etc. called before code runOnUiThread ever run. That mean the state already restore before runOnUiThread called. – Pongpat May 06 '15 at 14:48
  • You might want to read http://www.androiddesignpatterns.com/2013/08/fragment-transaction-commit-state-loss.html, a great article about fragment state loss. – Pongpat May 06 '15 at 14:51
  • About twigstechtips website, isn't it better to use the handler instead of trusting a thread, which you don't know when its execution will occur? About the androiddesignpatterns website, they don't talk about showing a DialogFragment in a fragment. Is it the same as for the activity: " However, as your transactions begin to venture out into the other Activity lifecycle methods, such as onActivityResult(), onStart(), and onResume(), things can get a little tricky." ? – android developer May 06 '15 at 20:30
  • About twigstechtips website, yes I have tried using handler before and it also work. However, it still does not look nice depending on Thread or Handler. About the androiddesignpatterns website, I just want to gave the idea regarding the issue. If you use getFragmentManager in the fragment it should be the same as for the activity, but if you use getChildFragmentManager I am not exactly sure. – Pongpat May 07 '15 at 02:10
  • You used a single call to "post(runnable)" or 2 calls to it? About "getFragmentManager ", so... what is advised about showing DialogFragments inside fragments code ? – android developer May 07 '15 at 10:46
  • 2
    I use single call to post(runnable). Regarding getFragmentManager, it depends. If you want to share that dialog with another activity you should use getFragmentManager, however, if that dialog only exist with fragment getChildFragmentManager seem a better choice. – Pongpat May 07 '15 at 11:12
  • so the solution is to call handler.post(...) within the onStart of the fragment (or any other function that's used to show the dialogFragment) ? – android developer May 07 '15 at 11:17
  • Copy from above "Our solution is create activity and fragment base class and delegate onResumeFragments to fragment (we create onResumeFragments in fragment base class). It is not nice solution but it does work.". Handler.post is our old solution, it does work, but does not look nice so we change. – Pongpat May 07 '15 at 11:39
  • I see. Is this behavior a bug or intentional? Also, is it possible that the DialogFragment of the support library is better, and might be able to handle this issue anyway ? – android developer May 07 '15 at 12:51
  • Also, I think an easier way would be to extend the DialogFragment (which I do anyway) to a base one, which for the "show" function will use the handler.post(...). – android developer May 07 '15 at 12:57
  • IllegalStateException is intentionally thrown. Because the state is lost and may end up in bad user experience. – Pongpat May 07 '15 at 14:44
  • State of what? I just show a dialog... Also, how could it be lost, if I'm in "onStart" ? isn't "onSavedInstanceState" supposed to be called when the activity is about to be destroyed, and not in its beginning? – android developer May 08 '15 at 10:48
  • Sometime, for example, when you rotate screen, the Fragment can create before activity that and also leads to state lost. – Pongpat May 08 '15 at 12:20
  • But "onStart" means it's about to be shown. How could it be shown if it's not attached to an activity that's now created: http://developer.android.com/guide/components/fragments.html – android developer May 09 '15 at 08:56
  • even after using the "Handler.postRunnable" solution, the exception still occurs: "IllegalStateException: Can not perform this action after onSaveInstanceState" . :( – android developer May 13 '15 at 10:47
  • Sadly, no, even if I was allowed (and I'm not, because it's closed sourced) I can't, because the "handler" solution actually hidden the place that the dialogFragment is trying to be shown (the stack trace doesn't show anything of the app itself, so I can't tell who called "show()"). However, what I can say is that before adding this workaround, I had a scenario: an AppCompatActivity which has a support-library fragment that was added in the backstack. This fragment tried to show a dialogFragment (of the normal API) in its "onStart" method. – android developer May 13 '15 at 11:22
  • I will now try to use the support-library dialogFragment. Hope this helps. Maybe I could at least tell Google to fix this issue. – android developer May 13 '15 at 11:23
  • I've put an issue of this here: https://code.google.com/p/android/issues/detail?id=173117 , containing some more information. – android developer May 13 '15 at 11:37
  • I've also found those links: https://groups.google.com/forum/#!topic/android-developers/dXZZjhRjkMk http://android.codeandmagic.org/android-dialogfragment-confuses-part-2/ . It seems I would need to either be very careful using fragments, or avoid using DialogFragments on some cases. Someone wrote that "dismissAllowingStateLoss" can be used, but not recommended, yet there wasn't an explanation for why. In any case, the exception I've described is for showing the dialog, which is weird but might have the same cause – android developer May 13 '15 at 12:59
22
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

Shripad Bhat
  • 2,373
  • 2
  • 19
  • 21
huu duy
  • 2,049
  • 21
  • 31
17

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) {

        }

    }
Dennis Zinkovski
  • 1,821
  • 3
  • 25
  • 42
  • 6
    So that developers don't have to inherit from `DialogFragment` you could change this to be a Kotlin extension function with the following signature: `fun DialogFragment.showAllowingStateLoss(fragmentManager: FragmentManager, tag: String)`. Also, the try-catch is not necessary since you're calling the `commitAllowingStateLoss()` method and not the `commit()` method. – Adil Hussain Aug 02 '19 at 18:23
13

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;
}
Frank
  • 12,010
  • 8
  • 61
  • 78
11

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);
    }
};
swooby
  • 3,005
  • 2
  • 36
  • 43
  • 3
    I love people that down vote with no explanation. Instead of *just* down voting, maybe it would be better if they explain how my solution is flawed? Can I down vote a down voter's down vote? – swooby Jun 22 '18 at 21:40
  • 1
    Yes, it's a problem of SO, I write this problem every time in suggestions, but they don't want to solve. – CoolMind Feb 01 '19 at 08:00
  • 3
    I think downvotes may be a result of the embedded XKCD, answers really aren't the place for social comments, (no matter how funny and/or true). – RestingRobot Mar 21 '19 at 19:03
  • Should this be on a BaseFragment(BaseActivity) or CustomDialogFragment or myFragment which is extends BaseFragment ? – fatihberatcan Mar 09 '21 at 11:06
7

Make your dialog fragment object global and call dismissAllowingStateLoss() in onPause() method

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

    if (dialogFragment != null) {
        dialogFragment.dismissAllowingStateLoss();
    }
}

Don't forget to assign value in fragment and call show() on button click or where ever.

Rohit Rajpal
  • 106
  • 1
  • 4
7

If you override show() function, DON'T DO THIS:

override fun show(manager: FragmentManager, tag: String?) {
    // mDismissed = false; is removed -> lead to wrong state
    // mShownByMe = true; is removed -> lead to wrong state
    val ft = manager.beginTransaction()
    ft.add(this, tag)
    ft.commitAllowingStateLoss()
}

It maybe lead to wrong state of dialog

Just do:

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

please try to use FragmentTransaction instead of FragmentManager. I think the below code will solve your problem. If not, Please let me know.

FragmentTransaction ft = getSupportFragmentManager().beginTransaction();
SnoozeDialog snoozeDialog = new SnoozeDialog();
snoozeDialog.show(ft, "snooze_dialog");

EDIT:

Fragment Transaction

Please check this link. I think it will solve you queries.

RIJO RV
  • 806
  • 6
  • 18
  • 5
    Any explanation on why using FragmentTransaction fixes the issue would be great. – Hemanshu Feb 10 '14 at 07:56
  • 4
    Dialog#show(FragmentManager, tag) does the same thing. This is not a solution. – William Dec 06 '14 at 07:29
  • 4
    This answer is not the solution. DialogFragment#show(ft) and show(fm) do the exact same thing. – danijoo Feb 02 '15 at 09:28
  • @danijoo You're right that both of that does the same job. But in few phones, there is some problem similar to this if you are using fragmentmanager instead of fragmenttransaction. So in my case, this solved my issue. – RIJO RV Feb 02 '15 at 10:06
4

use this code

FragmentTransaction ft = fm.beginTransaction();
        ft.add(yourFragment, "fragment_tag");
        ft.commitAllowingStateLoss();

instead of

yourFragment.show(fm, "fragment_tag");
Fakhriddin Abdullaev
  • 4,169
  • 2
  • 35
  • 37
3

Many views post high-level events such as click handlers to the event queue to run deferred. So the problem is that "onSaveInstanceState" has already been called for the Activity but the event queue contains deferred "click event". Hence when this event is dispatched to your handler

at android.os.Handler.handleCallback(Handler.java:605)
at android.os.Handler.dispatchMessage(Handler.java:92)
at android.os.Looper.loop(Looper.java:137)

and your code does show the IllegalStateException is thrown.

The simplest solution is to clean event queue, in onSaveInstanceState

protected void onSaveInstanceState(Bundle outState) {
        super.onSaveInstanceState(outState);
        // ..... do some work
        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) {
            findViewById(android.R.id.content).cancelPendingInputEvents();
        }
}
sim
  • 756
  • 6
  • 18
  • Have you actually confirmed that this solves the problem? – mhsmith Aug 31 '19 at 22:06
  • Google has added this to the next release of the androidx libraries, currently in beta ([`activity`](https://developer.android.com/jetpack/androidx/releases/activity#1.0.0-alpha01) and [`fragment`](https://developer.android.com/jetpack/androidx/releases/fragment#1.1.0-alpha01)). – mhsmith Aug 31 '19 at 23:55
  • 1
    @mhsmith I do remember that this solution solved problem in my code with IllegalStateException – sim Sep 02 '19 at 05:09
1

Though it's not officially mentioned anywhere but I faced this problem couple of times. In my experience there is something wrong in compatibility library supporting fragments on older platforms which causes this problem. You use test this by using normal fragment manager API. If nothing works then you can use the normal dialog instead of dialog fragment.

Dalvinder Singh
  • 2,129
  • 4
  • 21
  • 20
1
  1. Add this class to your project: (must be in android.support.v4.app package)
package android.support.v4.app;


/**
 * Created by Gil on 8/16/2017.
 */

public class StatelessDialogFragment extends DialogFragment {
    /**
     * Display the dialog, adding the fragment using an existing transaction and then committing the
     * transaction whilst allowing state loss.
* * I would recommend you use {@link #show(FragmentTransaction, String)} most of the time but * this is for dialogs you reallly don't care about. (Debug/Tracking/Adverts etc.) * * @param transaction * An existing transaction in which to add the fragment. * @param tag * The tag for this fragment, as per * {@link FragmentTransaction#add(Fragment, String) FragmentTransaction.add}. * @return Returns the identifier of the committed transaction, as per * {@link FragmentTransaction#commit() FragmentTransaction.commit()}. * @see StatelessDialogFragment#showAllowingStateLoss(FragmentManager, String) */ public int showAllowingStateLoss(FragmentTransaction transaction, String tag) { mDismissed = false; mShownByMe = true; transaction.add(this, tag); mViewDestroyed = false; mBackStackId = transaction.commitAllowingStateLoss(); return mBackStackId; } /** * Display the dialog, adding the fragment to the given FragmentManager. This is a convenience * for explicitly creating a transaction, adding the fragment to it with the given tag, and * committing it without careing about state. This does not add the transaction to the * back stack. When the fragment is dismissed, a new transaction will be executed to remove it * from the activity.
* * I would recommend you use {@link #show(FragmentManager, String)} most of the time but this is * for dialogs you reallly don't care about. (Debug/Tracking/Adverts etc.) * * * @param manager * The FragmentManager this fragment will be added to. * @param tag * The tag for this fragment, as per * {@link FragmentTransaction#add(Fragment, String) FragmentTransaction.add}. * @see StatelessDialogFragment#showAllowingStateLoss(FragmentTransaction, String) */ public void showAllowingStateLoss(FragmentManager manager, String tag) { mDismissed = false; mShownByMe = true; FragmentTransaction ft = manager.beginTransaction(); ft.add(this, tag); ft.commitAllowingStateLoss(); } }
  1. Extend StatelessDialogFragment instead of DialogFragment
  2. Use the method showAllowingStateLoss instead of show

  3. Enjoy ;)

Gil SH
  • 3,789
  • 1
  • 27
  • 25
  • What is these all boolean fields for?Why aren't they declared as class members? – undefined Sep 06 '17 at 07:21
  • 1
    The boolean fields are protected members of DialogFragment, their names obviously suggest what they are for and we need to update them in order to not interfere with the logic of DialogFragment. Note that in the original DialogFragment class, this functions exist but without public access – Gil SH Sep 07 '17 at 06:47
  • Ough these members are not protected, they are internal.I was getting compile errors as I put `StatelessDialogFragment` inside one of my packages.Thanks dude.I'll test it in production soon. – undefined Sep 07 '17 at 14:39
1

I have found an elegant solution for this problem by using reflection. Problem of all above solutions is that fields mDismissed and mShownByMe do not change their state.

Just override method "show" in your own custom bottom sheet dialog fragment like sample below (Kotlin)

override fun show(manager: FragmentManager, tag: String?) {
        val mDismissedField = DialogFragment::class.java.getDeclaredField("mDismissed")
        mDismissedField.isAccessible = true
        mDismissedField.setBoolean(this, false)

        val mShownByMeField = DialogFragment::class.java.getDeclaredField("mShownByMe")
        mShownByMeField.isAccessible = true
        mShownByMeField.setBoolean(this, true)

        manager.beginTransaction()
                .add(this, tag)
                .commitAllowingStateLoss()
    }
0

The following implementation can be used to solve the problem of performing safely state changes during the Activity lifecycle, in particular for showing dialogs: if the instance state has already been saved (e.g. due to a configuration change), it postpones them until the resumed state has been performed.

public abstract class XAppCompatActivity extends AppCompatActivity {

    private String TAG = this.getClass().getSimpleName();

    /** The retained fragment for this activity */
    private ActivityRetainFragment retainFragment;

    /** If true the instance state has been saved and we are going to die... */
    private boolean instanceStateSaved;

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

        // get hold of retain Fragment we'll be using
        retainFragment = ActivityRetainFragment.get(this, "Fragment-" + this.getClass().getName());
    }

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

        // reset instance saved state
        instanceStateSaved = false;

        // execute all the posted tasks
        for (ActivityTask task : retainFragment.tasks) task.exec(this);
        retainFragment.tasks.clear();
    }

    @Override
    protected void onSaveInstanceState(Bundle outState) {
        super.onSaveInstanceState(outState);
        instanceStateSaved = true;
    }

    /**
     * Checks if the activity state has been already saved.
     * After that event we are no longer allowed to commit fragment transactions.
     * @return true if the instance state has been saved
     */
    public boolean isInstanceStateSaved() {
        return instanceStateSaved;
    }

    /**
     * Posts a task to be executed when the activity state has not yet been saved
     * @param task The task to be executed
     * @return true if the task executed immediately, false if it has been queued
     */
    public final boolean post(ActivityTask task)
    {
        // execute it immediately if we have not been saved
        if (!isInstanceStateSaved()) {
            task.exec(this);
            return true;
        }

        // save it for better times
        retainFragment.tasks.add(task);
        return false;
    }

    /** Fragment used to retain activity data among re-instantiations */
    public static class ActivityRetainFragment extends Fragment {

        /**
         * Returns the single instance of this fragment, creating it if necessary
         * @param activity The Activity performing the request
         * @param name The name to be given to the Fragment
         * @return The Fragment
         */
        public static ActivityRetainFragment get(XAppCompatActivity activity, String name) {

            // find the retained fragment on activity restarts
            FragmentManager fm = activity.getSupportFragmentManager();
            ActivityRetainFragment fragment = (ActivityRetainFragment) fm.findFragmentByTag(name);

            // create the fragment and data the first time
            if (fragment == null) {
                // add the fragment
                fragment = new ActivityRetainFragment();
                fm.beginTransaction().add(fragment, name).commit();
            }

            return fragment;
        }

        /** The queued tasks */
        private LinkedList<ActivityTask> tasks = new LinkedList<>();

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

            // retain this fragment
            setRetainInstance(true);
        }

    }

    /** A task which needs to be performed by the activity when it is "fully operational" */
    public interface ActivityTask {

        /**
         * Executed this task on the specified activity
         * @param activity The activity
         */
        void exec(XAppCompatActivity activity);
    }
}

Then using a class like this:

/** AppCompatDialogFragment implementing additional compatibility checks */
public abstract class XAppCompatDialogFragment extends AppCompatDialogFragment {

    /**
     * Shows this dialog as soon as possible
     * @param activity The activity to which this dialog belongs to
     * @param tag The dialog fragment tag
     * @return true if the dialog has been shown immediately, false if the activity state has been saved
     *         and it is not possible to show it immediately
     */
    public boolean showRequest(XAppCompatActivity activity, final String tag) {
        return showRequest(activity, tag, null);
    }

    /**
     * Shows this dialog as soon as possible
     * @param activity The activity to which this dialog belongs to
     * @param tag The dialog fragment tag
     * @param args The dialog arguments
     * @return true if the dialog has been shown immediately, false if the activity state has been saved
     *         and it is not possible to show it immediately
     */
    public boolean showRequest(XAppCompatActivity activity, final String tag, final Bundle args)
    {
        return activity.post(new XAppCompatActivity.ActivityTask() {
            @Override
            public void exec(XAppCompatActivity activity) {
                if (args!= null) setArguments(args);
                show(activity.getSupportFragmentManager(), tag);
            }
        });
    }

    /**
     * Dismiss this dialog as soon as possible
     * @return true if the dialog has been dismissed immediately, false if the activity state has been saved
     *         and it is not possible to dismissed it immediately
     */
    public boolean dismissRequest()
    {
        return dismissRequest(null);
    }

    /**
     * Dismiss this dialog as soon as possible
     * @param runnable Actions to be performed before dialog dismissal
     * @return true if the dialog has been dismissed immediately, false if the activity state has been saved
     *         and it is not possible to dismissed it immediately
     */
    public boolean dismissRequest(final Runnable runnable)
    {
        // workaround as in rare cases the activity could be null
        XAppCompatActivity activity = (XAppCompatActivity)getActivity();
        if (activity == null) return false;

        // post the dialog dismissal
        return activity.post(new XAppCompatActivity.ActivityTask() {
            @Override
            public void exec(XAppCompatActivity activity) {
                if (runnable != null) runnable.run();
                dismiss();
            }
        });
    }
}

You can safely show dialogs without worrying about the app state:

public class TestDialog extends XAppCompatDialogFragment {

    private final static String TEST_DIALOG = "TEST_DIALOG";

    public static void show(XAppCompatActivity activity) {
        new TestDialog().showRequest(activity, TEST_DIALOG);
    }

    public TestDialog() {}

    @NonNull
    @Override
    public Dialog onCreateDialog(Bundle savedInstanceState)
    {
        return new AlertDialog.Builder(getActivity(), R.style.DialogFragmentTheme /* or null as you prefer */)
                .setTitle(R.string.title)
                // set all the other parameters you need, e.g. Message, Icon, etc.
                ).create();
    }
}

and then call TestDialog.show(this) from within your XAppCompatActivity.

If you want to create a more generic dialog class with parameters, you can save them in a Bundle with the arguments in the show() method and retrieve them with getArguments() in onCreateDialog().

The whole approach could seem a bit complex, but once you have created the two base classes for activities and dialogs, it is quite easy to use and is perfectly working. It can be used for other Fragment based operations which could be affected by the same problem.

gxcare
  • 511
  • 4
  • 12
0

This error appears to be occurring because input events (such as key down or onclick events) are getting delivered after onSaveInstanceState is called.

The solution is to override onSaveInstanceState in your Activity and cancel any pending events.

@Override
protected void onSaveInstanceState(Bundle outState) {
    super.onSaveInstanceState(outState);
    if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) {
        final View rootView = findViewById(android.R.id.content);
        if (rootView != null) {
            rootView.cancelPendingInputEvents();
        }
    }
}
William
  • 20,150
  • 8
  • 49
  • 91