web-dev-qa-db-fra.com

Lors de l'affichage du dialogue, j'obtiens "Impossible d'effectuer cette action après onSaveInstanceState"

Certains utilisateurs signalent que s'ils utilisent l'action rapide de la barre de notification, ils se ferment de force.

Je montre une action rapide dans la notification qui appelle le "TestDialog" class . Dans la classe TestDialog après avoir appuyé sur le bouton "répéter", je montrerai le 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");
}

L'erreur est *IllegalStateException: Can not perform this action after onSaveInstanceState*.

La ligne de code où se déclenche la IllegarStateException est:

snoozeDialog.show(fm, "snooze_dialog");

La classe étend "FragmentActivity" et la classe "SnoozeDialog" est étendue "DialogFragment".

Voici la trace complète de la pile de l'erreur:

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)

Je ne peux pas reproduire cette erreur, mais je reçois beaucoup de rapports d'erreur.

Quelqu'un peut-il aider cela, comment puis-je réparer cette erreur?

88
chrisonline

Ceci est courant issue . Nous avons résolu ce problème en surchargeant show () et en gérant les exceptions dans la classe étendue DialogFragment

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);
        }
    }
}

Notez que l'application de cette méthode ne modifiera pas les champs internes du DialogFragment.class:

boolean mDismissed;
boolean mShownByMe;

Cela peut conduire à des résultats inattendus dans certains cas. Mieux vaut commitAllowingStateLoss () au lieu de commit ()

42
Rafael

Cela signifie que vous commit() (show() en cas de DialogFragment) fragmentez après onSaveInstanceState().

Android enregistrera votre état de fragment à onSaveInstanceState(). Donc, si vous commit() fragment après onSaveInstanceState() état du fragment sera perdu.

En conséquence, si Activity est tué et est recréé plus tard, le fragment n’ajoutera aucune activité, ce qui constitue une mauvaise expérience utilisateur. C'est pourquoi Android ne permet pas la perte d'état à tout prix.

La solution simple consiste à vérifier si l'état est déjà enregistré.

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");
    }
}

Remarque: onResumeFragments () appellera à la reprise des fragments.

21
Pongpat
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

11
huu duy

Si le dialogue n'est pas vraiment important (vous pouvez ne pas l'afficher lorsque l'application est fermée/n'est plus en vue), utilisez:

boolean running = false;

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

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

Et ouvrez votre dialogue (fragment) uniquement lorsque nous courons:

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

EDITER, SOLUTION PROBABLEMENT MEILLEURE:

Où onSaveInstanceState est appelé dans le cycle de vie est imprévisible, je pense qu'une meilleure solution consiste à vérifier isSavedInstanceStateDone () comme ceci:

/**
 * 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;
}
9
Frank

Après quelques jours, je souhaite partager ma solution comme je l'ai corrigée. Pour indiquer à DialogFragment que vous devez en remplacer la méthode show() et appeler commitAllowingStateLoss() sur l'objet Transaction. Voici l'exemple à Kotlin:

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

        }

    }
9
Dennis Zinkovski

veuillez essayer d'utiliser FragmentTransaction au lieu de FragmentManager. Je pense que le code ci-dessous va résoudre votre problème. Si non, s'il vous plaît faites le moi savoir.

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

MODIFIER:

Fragment Transaction

S'il vous plaît vérifier ce lien. Je pense que cela va résoudre vos questions.

5
RIJO RV

J'ai couru dans ce problème pendant des années.
Les internets sont parsemés de notes (des centaines? Des milliers?) De discussions à ce sujet, et la confusion et la désinformation en eux semblent se multiplier.
Pour aggraver la situation, et dans l’esprit du comique "14 standards" de xkcd, je lance ma réponse sur le ring.
 xkcd 14 standards

Les solutions cancelPendingInputEvents(), commitAllowingStateLoss(), catch (IllegalStateException e) et les solutions similaires semblent toutes atroces.

Espérons que ce qui suit montre facilement comment reproduire et résoudre le problème: 

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);
    }
};
4
swooby
  1. Ajoutez cette classe à votre projet: (doit être dans Android.support.v4.app package)
 package Android.support.v4.app; 


/** 
 * Créé par Gil le 16/08/2017 .
 */

 public class StatelessDialogFragment étend DialogFragment {
 /**
 * Affichez la boîte de dialogue en ajoutant le fragment à l'aide d'une transaction existante, puis en validant le 
 * transaction tout en permettant la perte de l'état.
* * Je vous recommande d'utiliser {@link #show (FragmentTransaction, String)} la plupart du temps, mais * ceci est destiné aux dialogues qui ne vous intéressent vraiment pas. (Débogage/Suivi/Annonces etc.) * * @param transaction * Une transaction existante dans laquelle ajouter le fragment . * @param tag * La balise pour ce fragment, conformément à * {@link FragmentTransaction # add (Fragment, String) FragmentTransaction.add} . * @return Renvoie l'identifiant de la transaction validée, conformément à * {@link FragmentTransaction # commit () FragmentTransaction.commit ()} . * @see StatelessDialogFragment # showAllowingStateLoss (FragmentManager, String) */ public int showAllowingStateLoss (transaction FragmentTransaction, balise String) { mDissus = faux; mShownByMe = true; transaction.add (this, tag); mViewDestroyed = false; mBackStackId = transaction.commitAllowingStateLoss (); renvoyer mBackStackId; } /** * Afficher la boîte de dialogue, en ajoutant le fragment au FragmentManager donné. C'est une commodité * pour créer explicitement une transaction, y ajouter le fragment avec la balise donnée et * le commettre sans se soucier de l'état. Ceci ne pas ajoute la transaction au fichier * pile de retour. Lorsque le fragment est rejeté, une nouvelle transaction sera exécutée pour le supprimer * de l'activité.
* * Je vous recommande d'utiliser {@link #show (FragmentManager, String)} la plupart du temps, mais il s'agit de * pour les dialogues qui ne vous intéressent vraiment pas. (Débogage/Suivi/Annonces etc.) * * * @param manager * Le FragmentManager auquel ce fragment sera ajouté à . * @param tag * La balise pour ce fragment, conformément à * {@link FragmentTransaction # add (Fragment, String) FragmentTransaction.add} . * @see StatelessDialogFragment # showAllowingStateLoss (FragmentTransaction, String) */ showAllowingStateLoss (gestionnaire FragmentManager, balise String) { mDissus = faux; mShownByMe = true; FragmentTransaction ft = manager.beginTransaction (); ft.add (this, tag); ft.commitAllowingStateLoss (); } }
  1. Étendre StatelessDialogFragment au lieu de DialogFragment 
  2. Utilisez la méthode showAllowingStateLoss au lieu de show

  3. Prendre plaisir ;)

1
Gil SH

Bien que ce ne soit pas officiellement mentionné nulle part, j'ai fait face à ce problème à plusieurs reprises. D'après mon expérience, il y a quelque chose qui cloche dans la bibliothèque de compatibilité prenant en charge des fragments sur des plateformes plus anciennes qui cause ce problème. Vous utilisez test this en utilisant une API de gestionnaire de fragments normale. Si rien ne fonctionne, vous pouvez utiliser le dialogue normal au lieu du fragment de dialogue.

1
Dalvinder Singh

De nombreuses vues publient des événements de haut niveau, tels que des gestionnaires de clics, dans la file d'événements à exécuter en différé. Le problème est donc que "onSaveInstanceState" a déjà été appelé pour l'activité mais que la file d'attente d'événements contient un "événement de clic" différé. Par conséquent, lorsque cet événement est envoyé à votre gestionnaire 

at Android.os.Handler.handleCallback(Handler.Java:605)
at Android.os.Handler.dispatchMessage(Handler.Java:92)
at Android.os.Looper.loop(Looper.Java:137)

et votre code ne show est levée IllegalStateException.

La solution la plus simple consiste à nettoyer la file d'attente des événements, dans 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();
        }
}
0
sim

Rendez votre objet fragment de dialogue global et appelez la méthode rejetAllowingStateLoss () dans la méthode onPause ()

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

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

N'oubliez pas d'attribuer une valeur à fragment et appelez show () en cliquant sur un bouton ou ailleurs.

0
Rohit Rajpal

Cette erreur semble se produire car les événements d'entrée (tels que les événements key down ou onclick) sont remis après l'appel de onSaveInstanceState.

La solution consiste à remplacer onSaveInstanceState dans votre activité et à annuler tout événement en attente.

@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();
        }
    }
}
0
William

utiliser ce code

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

au lieu de

yourFragment.show(fm, "fragment_tag");
0

L’implémentation suivante peut être utilisée pour résoudre le problème des changements d’état en toute sécurité pendant le cycle de vie Activity, en particulier pour l’affichage des dialogues: si l’état de l’instance a déjà été sauvegardé (suite à un changement de configuration, par exemple), il est reporté au retour a été effectuée.

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);
    }
}

Ensuite, en utilisant une classe comme celle-ci:

/** 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();
            }
        });
    }
}

Vous pouvez afficher en toute sécurité des dialogues sans vous soucier de l'état de l'application:

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

puis appelez TestDialog.show(this) à partir de votre XAppCompatActivity.

Si vous souhaitez créer une classe de dialogue plus générique avec des paramètres, vous pouvez les enregistrer dans une variable Bundle avec les arguments de la méthode show() et les extraire avec getArguments() dans onCreateDialog().

Cette approche peut sembler un peu complexe, mais une fois que vous avez créé les deux classes de base pour les activités et les dialogues, il est assez facile à utiliser et fonctionne parfaitement. Il peut être utilisé pour d'autres opérations basées sur Fragment qui pourraient être affectées par le même problème.

0
gicci