web-dev-qa-db-fra.com

commitAllowingStateLoss () et le fragment commit ()

Je veux commettre un fragment après une opération en arrière-plan du réseau. J'appelais commit () après une opération réseau réussie, mais dans le cas où l'activité se mettait en pause ou à l'arrêt, l'application bloquait en disant l'exception IllegalState.

Alors j'ai essayé d'utiliser commitAllowingStateLoss () et tout fonctionne bien maintenant.

J'ai parcouru peu de blogs et d'articles, il est écrit que commitAllowingStateLoss () n'est pas bon à utiliser.

Quel est le moyen de gérer le fragment de validation après la mise en pause et l'arrêt de l'activité du réseau?

18
amodkanthe

J'aimerais ajouter des informations à Aritra Roy (jusqu'à présent, j'ai lu, c'est une très bonne réponse).

J'ai déjà rencontré le problème auparavant, et le problème principal est que vous essayez d'effectuer des opérations asynchrones (HTTP, calculs, ...) dans un autre thread, ce qui est une bonne pratique, mais vous devez informer votre utilisateur APRÈS avoir reçu. réponses.

Le problème principal est qu’étant donné qu’il s’agit d’opérations asynchrones, il n’ya aucune garantie que l’utilisateur soit toujours sur votre activité/application. Et s’il est parti, il n’est pas nécessaire de faire des changements d’assurance-chômage. De plus, comme Android peut tuer votre application/activité pour des problèmes de mémoire, vous n’aurez aucune garantie de pouvoir obtenir votre réponse et sauvegardez-la pour qu'elle soit restaurée ..__ Le problème est non seulement "l'utilisateur peut ouvrir une autre application" mais "mon activité peut être recréée à partir d'un changement de configuration" et vous essayez peut-être de modifier l'interface utilisateur pendant la récréation d'activité, ce qui serait vraiment très mauvais.

Utiliser "commitAllowingStateLoss" revient à dire "peu m'importe si l'interface utilisateur n'est pas vraiment en bon état". Vous pouvez le faire pour de petites choses (comme activer un gif indiquant que votre téléchargement prend fin) ... Ce n'est pas un gros problème, et ce problème ne vaut pas vraiment la peine d'être traité car "en général", l'utilisateur restera sur votre application.

Mais, l'utilisateur a fait quelque chose, vous essayez d'obtenir des informations sur le Web, les informations sont prêtes et vous devez les montrer lorsque l'utilisateur reprend l'application ... le mot principal est "CV". .

Vous devez rassembler les données dont vous avez besoin dans une variable (et si vous le pouvez, une variable parcellaire ou primitive), puis remplacer les fonctions "onResume" ou "onPostResume" (pour les activités) de la manière suivante.

public void onResume/onPostResume() {
    super.onResume/onPostResume();
    if(someTreatmentIsPending) {
        /*do what you need to do with your variable here : fragment 
        transactions, dialog showing...*/
    }
}

Informations complémentaires:Ce sujet et plus particulièrement @jed answer, et @pjv, @Sufian commente . Ce blog afin de comprendre pourquoi le bogue se produit, et pourquoi les réponses proposées/acceptées fonctionnent.

Dernier mot: Juste au cas où vous vous demanderiez "pourquoi utiliser un service est préférable à asyncTask". Pour ce que j'ai compris, ce n'est pas vraiment mieux. La principale différence réside dans le fait que l’utilisation du service vous permet d’enregistrer/désinscrire des gestionnaires lorsque votre activité est suspendue/reprise. Par conséquent, vous obtenez toujours vos réponses lorsque votre activité est active, empêchant ainsi le bogue de se produire.

Notez que ce n'est pas parce que le bogue ne se produit pas que vous êtes en sécurité. Si vous apportez des modifications directement dans vos vues, aucune transaction fragmentTransactions n'est impliquée. Par conséquent, rien ne garantit que la modification sera conservée et recréée lorsque l'application est recréée, reprise, relancée ou quoi que ce soit d'autre.

2
Feuby

Je vais essayer de vous expliquer tout en détail.

FragmentTransaction dans la bibliothèque de support fournit quatre méthodes pour valider une transaction,

1) commit ()

2) commitAllowingStateLoss ()

3) commitNow ()

4) commitNowAllowingStateLoss ()

Vous obtenez probablement un IllegalStateException disant que vous ne pouvez pas valider après l'appel de onSaveInstanceState(). Vérifiez this post qui décrit pourquoi cette exception est levée en premier lieu.

Les fonctions commit() et commitAllowingStateLoss() sont presque identiques dans leur implémentation} avec une différence, commit() vérifie si l'état a déjà été enregistré et, le cas échéant, il renvoie IllegalStateException. Vous pouvez vérifier le code source vous-même.

Alors, perdez-vous cet état chaque fois que vous appelez commitAllowingStateLoss(). Non, certainement pas. Vous POUVEZ perdre l'état de FragmentManager ou de tout autre fragment ajouté ou supprimé après onSaveInstanceState () si l'application est supprimée.

Voici un scénario pratique pour vous cas d'utilisation -

  • Votre activité montre FragmentA
  • Votre activité passe à l’arrière-plan et onSaveInstanceState() obtient Appelé.
  • Votre opération réseau est terminée et vous remplacez FragmentA par FragmentB en utilisant commitAllowingStateLoss()

Ce sont les deux choses possibles qui peuvent arriver maintenant -

  • Si le système a tué votre application en raison d'un manque de mémoire, votre application Sera recréée à nouveau avec l'état de sauvegarde créé à l'étape 2. . FragmentB ne sera pas visible. _ {C'est quand vous perdez cet état}.

  • Si le système n'a pas tué votre application et qu'elle est toujours en mémoire, Elle sera alors ramenée au premier plan et FragmentB restera affiché _ {Dans ce cas, vous n'avez pas perdu l'état.} _

Vous pouvez vérifier this Github project et essayer vous-même ce scénario. 

Si vous avez activé l’option de développeur “Ne pas garder les activités”, vous rencontrerez le premier scénario dans lequel l’état est réellement perdu. 

Si vous avez activé cette option, vous rencontrerez le deuxième scénario dans lequel aucun état n'est perdu.

19
Aritra Roy

J'ai rencontré le même problème et trouvé une solution de contournement très simple. Étant donné qu'Android OS n'a pas de solution à ce moment-là (n'en avez toujours pas, mais commitAllowingStateLoss() est l'une de leurs solutions et vous connaissez le reste). 

La solution consistait à écrire une classe Handler qui tamponne les messages lorsque l'activité est transmise et les lit à nouveau sur onResume.

En utilisant cette classe, assurez-vous que tout le code dont asynchronously change fragment state (commit etc.) est appelé à partir d'un message contenu dans ce gestionnaire `.

ExtendFragmenntPauseHandler à partir de la classe de descripteurs.

Chaque fois que votre activité reçoit un onPause(), appelez FragmenntPauseHandler.pause() et pour onResume(), appelez FragmenntPauseHandler.resume().

Remplacez votre implémentation de Handler handleMessage() par processMessage().

Fournissez une implémentation simple de storeMessage() qui retourne toujours true.

/**
 * Message Handler class that supports buffering up of messages when the
 * activity is paused i.e. in the background.
 */
public abstract class FragmenntPauseHandler extends Handler {

    /**
     * Message Queue Buffer
     */
    final Vector<Message> messageQueueBuffer = new Vector<Message>();

    /**
     * Flag indicating the pause state
     */
    private boolean paused;

    /**
     * Resume the handler
     */
    final public void resume() {
        paused = false;

        while (messageQueueBuffer.size() > 0) {
            final Message msg = messageQueueBuffer.elementAt(0);
            messageQueueBuffer.removeElementAt(0);
            sendMessage(msg);
        }
    }

    /**
     * Pause the handler
     */
    final public void pause() {
        paused = true;
    }

    /**
     * Notification that the message is about to be stored as the activity is
     * paused. If not handled the message will be saved and replayed when the
     * activity resumes.
     * 
     * @param message
     *            the message which optional can be handled
     * @return true if the message is to be stored
     */
    protected abstract boolean storeMessage(Message message);

    /**
     * Notification message to be processed. This will either be directly from
     * handleMessage or played back from a saved message when the activity was
     * paused.
     * 
     * @param message
     *            the message to be handled
     */
    protected abstract void processMessage(Message message);

    /** {@inheritDoc} */
    @Override
    final public void handleMessage(Message msg) {
        if (paused) {
            if (storeMessage(msg)) {
                Message msgCopy = new Message();
                msgCopy.copyFrom(msg);
                messageQueueBuffer.add(msgCopy);
            }
        } else {
            processMessage(msg);
        }
    }
}

Vous trouverez ci-dessous un exemple simple d'utilisation de la classe PausedHandler.

Au clic d'une Button, un message différé est envoyé à la handler.

Lorsque handler reçoit le message (sur le fil de l'interface utilisateur), il affiche DialogFragment.

Si la classe FragmenntPauseHandler n'était pas utilisée, une IllegalStateException serait affichée si le bouton d'accueil était enfoncé après avoir appuyé sur le bouton de test pour ouvrir la boîte de dialogue.

public class FragmentTestActivity extends Activity {

    /**
     * Used for "what" parameter to handler messages
     */
    final static int MSG_WHAT = ('F' << 16) + ('T' << 8) + 'A';
    final static int MSG_SHOW_DIALOG = 1;

    int value = 1;

    final static class State extends Fragment {

        static final String TAG = "State";
        /**
         * Handler for this activity
         */
        public ConcreteTestHandler handler = new ConcreteTestHandler();

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

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

            handler.setActivity(getActivity());
            handler.resume();
        }

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

            handler.pause();
        }

        public void onDestroy() {
            super.onDestroy();
            handler.setActivity(null);
        }
    }

    /**
     * 2 second delay
     */
    final static int DELAY = 2000;

    /** Called when the activity is first created. */
    @Override
    public void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.main);

        if (savedInstanceState == null) {
            final Fragment state = new State();
            final FragmentManager fm = getFragmentManager();
            final FragmentTransaction ft = fm.beginTransaction();
            ft.add(state, State.TAG);
            ft.commit();
        }

        final Button button = (Button) findViewById(R.id.popup);

        button.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {

                final FragmentManager fm = getFragmentManager();
                State fragment = (State) fm.findFragmentByTag(State.TAG);
                if (fragment != null) {
                    // Send a message with a delay onto the message looper
                    fragment.handler.sendMessageDelayed(
                            fragment.handler.obtainMessage(MSG_WHAT, MSG_SHOW_DIALOG, value++),
                            DELAY);
                }
            }
        });
    }

    public void onSaveInstanceState(Bundle bundle) {
        super.onSaveInstanceState(bundle);
    }

    /**
     * Simple test dialog fragment
     */
    public static class TestDialog extends DialogFragment {

        int value;

        /**
         * Fragment Tag
         */
        final static String TAG = "TestDialog";

        public TestDialog() {
        }

        public TestDialog(int value) {
            this.value = value;
        }

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

        @Override
        public View onCreateView(LayoutInflater inflater, ViewGroup container,
                Bundle savedInstanceState) {
            final View inflatedView = inflater.inflate(R.layout.dialog, container, false);
            TextView text = (TextView) inflatedView.findViewById(R.id.count);
            text.setText(getString(R.string.count, value));
            return inflatedView;
        }
    }

    /**
     * Message Handler class that supports buffering up of messages when the
     * activity is paused i.e. in the background.
     */
    static class ConcreteTestHandler extends FragmenntPauseHandler {

        /**
         * Activity instance
         */
        protected Activity activity;

        /**
         * Set the activity associated with the handler
         * 
         * @param activity
         *            the activity to set
         */
        final void setActivity(Activity activity) {
            this.activity = activity;
        }

        @Override
        final protected boolean storeMessage(Message message) {
            // All messages are stored by default
            return true;
        };

        @Override
        final protected void processMessage(Message msg) {

            final Activity activity = this.activity;
            if (activity != null) {
                switch (msg.what) {

                case MSG_WHAT:
                    switch (msg.arg1) {
                    case MSG_SHOW_DIALOG:
                        final FragmentManager fm = activity.getFragmentManager();
                        final TestDialog dialog = new TestDialog(msg.arg2);

                        // We are on the UI thread so display the dialog
                        // fragment
                        dialog.show(fm, TestDialog.TAG);
                        break;
                    }
                    break;
                }
            }
        }
    }
}

J'ai ajouté une méthode storeMessage() à la classe FragmenntPauseHandler au cas où des messages devraient être traités immédiatement, même lorsque l'activité est suspendue. Si un message est traité, la valeur false doit être renvoyée et le message sera supprimé. J'ai utilisé celle-ci dans 4 applications et je n'ai plus jamais eu le même problème.

1
Nouman Ghaffar

Une manière simple serait d’attendre la reprise de Activity afin que vous puissiez commit votre action, une solution de contournement simple ressemblerait à ceci:

@Override
public void onNetworkResponse(){
       //Move to next fragmnt if Activity is Started or Resumed
       shouldMove = true;
       if (isResumed()){
            moveToNext = false;

            //Move to Next Page
            getActivity().getSupportFragmentManager()
                    .beginTransaction()
                    .replace(R.id.fragment_container, new NextFragment())
                    .addToBackStack(null)
                    .commit();
       }
}

Donc, si Fragment est repris (donc Activity), vous pouvez commit votre action mais sinon, vous attendez que Activity commence à commit votre action:

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

    if(moveToNext){
        moveToNext = false;

        //Move to Next Page
        getActivity().getSupportFragmentManager()
                .beginTransaction()
                .replace(R.id.fragment_container, new NextFragment())
                .addToBackStack(null)
                .commit();
    }
}

P.S: Faites attention à moveToNext = false; Il est là pour vous assurer qu'après commit, vous ne répéterez pas commit en cas de retour en utilisant la presse à retour.

0
Keivan Esbati

commit Planifie un commit de cette transaction. Le commit ne se produit pas immédiatement; il sera programmé en tant que travail sur le thread principal à effectuer la prochaine fois que ce thread est prêt.

Une transaction ne peut être validée avec cette méthode que si son activité contenante est sauvegardée. Si la validation est tentée après ce moment, une exception sera levée. Cela est dû au fait que l'état après la validation peut être perdu si l'activité doit être restaurée à partir de son état.

commitAllowingStateLoss

Identique à commit () mais permet l'exécution de la validation après la sauvegarde de l'état d'une activité. Cela est dangereux car la validation peut être perdue si l'activité doit être restaurée ultérieurement, elle ne doit donc être utilisée que dans les cas où l'état de l'interface utilisateur peut changer de manière inattendue pour l'utilisateur.

Et selon votre question, vous utilisez AsyncTask pour le fonctionnement en arrière-plan du réseau. Donc, cela pourrait être un problème que vous commettiez fragment dans sa méthode de rappel. Pour éviter cette exception, ne vous engagez pas dans les méthodes de rappel asynctask. Ce n'est pas une solution mais une précaution. 

0
taman neupane

Il suffit de vérifier si l'activité se termine ou non, puis commit() la transaction.

if (!isFinishing()) {
    // commit transaction
}

L'exception que vous obtenez est dans un scénario, lorsque l'activité est en cours, et que vous essayez de valider une nouvelle transaction, qui ne sera évidemment pas sauvegardée par FragmentManager car onSavedInstanceState() a déjà été exécuté. C'est pourquoi framework vous oblige à le mettre en œuvre "correctement".

0
azizbekian