Comment gérer les messages du gestionnaire lorsque l'activité / le fragment est suspendu

98

Légère variation sur mon autre message

Fondamentalement, j'ai un message Handlerdans mon Fragmentqui reçoit un tas de messages qui peuvent entraîner le rejet ou l'affichage des boîtes de dialogue.

Lorsque l'application est mise en arrière-plan, je reçois un onPausemessage mais je reçois toujours mes messages comme prévu. Cependant, comme j'utilise des fragments, je ne peux pas simplement ignorer et afficher les boîtes de dialogue car cela entraînera un fichier IllegalStateException.

Je ne peux pas simplement rejeter ou annuler l'autorisation de la perte d'état.

Étant donné que j'ai un, Handlerje me demande s'il existe une approche recommandée pour savoir comment gérer les messages en état de pause.

Une solution possible que j'envisage est d'enregistrer les messages qui arrivent pendant une pause et de les lire sur un fichier onResume. C'est quelque peu insatisfaisant et je pense qu'il doit y avoir quelque chose dans le cadre pour gérer cela de manière plus élégante.

PJL
la source
1
vous pouvez supprimer tous les messages du gestionnaire dans la méthode onPause () du fragment, mais il y a un problème de restauration des messages qui, à mon avis, n'est pas possible.
Yashwanth Kumar

Réponses:

167

Bien que le système d'exploitation Android ne semble pas avoir de mécanisme qui résout suffisamment votre problème, je pense que ce modèle fournit une solution de contournement relativement simple à mettre en œuvre.

La classe suivante est un wrapper android.os.Handlerqui met en mémoire tampon les messages lorsqu'une activité est suspendue et les lit à la reprise.

Assurez-vous que tout code dont vous disposez qui modifie de manière asynchrone un état de fragment (par exemple, validation, rejet) n'est appelé qu'à partir d'un message dans le gestionnaire.

Dérivez votre gestionnaire de la PauseHandlerclasse.

Chaque fois que votre activité reçoit un onPause()appel PauseHandler.pause()et pour onResume()appel PauseHandler.resume().

Remplacez votre implémentation du gestionnaire handleMessage()par processMessage().

Fournir une implémentation simple storeMessage()qui revient toujours true.

/**
 * Message Handler class that supports buffering up of messages when the
 * activity is paused i.e. in the background.
 */
public abstract class PauseHandler 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);
        }
    }
}

Voici un exemple simple de la façon dont la PausedHandlerclasse peut être utilisée.

En cliquant sur un bouton, un message différé est envoyé au gestionnaire.

Lorsque le gestionnaire reçoit le message (sur le thread d'interface utilisateur), il affiche un fichier DialogFragment.

Si la PausedHandlerclasse n'était pas utilisée, une exception IllegalStateException serait affichée si le bouton d'accueil était pressé après avoir appuyé sur le bouton de test pour lancer 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 PauseHandler {

        /**
         * 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 storeMessage()méthode à la PausedHandlerclasse au cas où des messages devraient être traités immédiatement, même lorsque l'activité est en pause. Si un message est traité, false doit être renvoyé et le message sera rejeté.

dégaine mcgraw
la source
26
Belle solution, fonctionne un régal. Je ne peux pas m'empêcher de penser que le cadre devrait gérer cela.
PJL
1
comment passer le rappel à DialogFragment?
Malachiasz
Je ne suis pas sûr de comprendre la question Malachiasz, pourriez-vous élaborer.
dégaine mcgraw
C'est une solution très élégante! Sauf erreur de ma part, car la resumeméthode utilise sendMessage(msg)techniquement il pourrait y avoir d'autres threads mettant en file d'attente le message juste avant (ou entre les itérations de la boucle), ce qui signifie que les messages stockés pourraient être entrelacés avec de nouveaux messages arrivant. Je ne sais pas si c'est un gros problème. Peut-être que l'utilisation sendMessageAtFrontOfQueue(et bien sûr l'itération vers l'arrière) résoudrait ce problème?
yan
4
Je pense que cette approche peut ne pas toujours fonctionner - si l'activité est détruite par le système d'exploitation, la liste des messages en attente d'être des processus sera vide après la reprise.
GaRRaPeTa
10

Une version légèrement plus simple de l'excellent PauseHandler de quickdraw est

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

    /**
     * Message Queue Buffer
     */
    private final List<Message> messageQueueBuffer = Collections.synchronizedList(new ArrayList<Message>());

    /**
     * Flag indicating the pause state
     */
    private Activity activity;

    /**
     * Resume the handler.
     */
    public final synchronized void resume(Activity activity) {
        this.activity = activity;

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

    /**
     * Pause the handler.
     */
    public final synchronized void pause() {
        activity = null;
    }

    /**
     * Store the message if we have been paused, otherwise handle it now.
     *
     * @param msg   Message to handle.
     */
    @Override
    public final synchronized void handleMessage(Message msg) {
        if (activity == null) {
            final Message msgCopy = new Message();
            msgCopy.copyFrom(msg);
            messageQueueBuffer.add(msgCopy);
        } else {
            processMessage(activity, msg);
        }
    }

    /**
     * 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 activity  Activity owning this Handler that isn't currently paused.
     * @param message   Message to be handled
     */
    protected abstract void processMessage(Activity activity, Message message);

}

Cela suppose que vous souhaitez toujours stocker les messages hors ligne pour la relecture. Et fournit l'activité comme entrée pour #processMessagesque vous n'ayez pas besoin de la gérer dans la sous-classe.

William
la source
Pourquoi vos resume()et pause(), et handleMessage synchronized?
Maksim Dmitriev
5
Parce que vous ne voulez pas que #pause soit appelé pendant #handleMessage et que vous découvrez soudainement que l'activité est nulle pendant que vous l'utilisez dans #handleMessage. C'est une synchronisation entre l'état partagé.
William
@William Pourriez-vous m'expliquer s'il vous plaît plus de détails pourquoi avez-vous besoin d'une synchronisation dans une classe PauseHandler? Il semble que cette classe ne fonctionne que dans un seul thread, le thread d'interface utilisateur. Je suppose que #pause n'a pas pu être appelé pendant #handleMessage car les deux fonctionnent dans le thread d'interface utilisateur.
Samik
@William êtes-vous sûr? HandlerThread handlerThread = new HandlerThread ("mHandlerNonMainThread"); handlerThread.start (); Looper boucleurNonMainThread = handlerThread.getLooper (); Handler handlerNonMainThread = new Handler (looperNonMainThread, new Callback () {public boolean handleMessage (Message msg) {return false;}});
swooby
Désolé @swooby je ne suis pas suivre. Suis-je sûr de quoi? Et quel est le but de l'extrait de code que vous avez publié?
William
2

Voici une manière légèrement différente d'aborder le problème des validations de Fragment dans une fonction de rappel et d'éviter le problème IllegalStateException.

Créez d'abord une interface exécutable personnalisée.

public interface MyRunnable {
    void run(AppCompatActivity context);
}

Ensuite, créez un fragment pour traiter les objets MyRunnable. Si l'objet MyRunnable a été créé après la mise en pause de l'activité, par exemple si l'écran pivote ou si l'utilisateur appuie sur le bouton d'accueil, il est placé dans une file d'attente pour un traitement ultérieur avec un nouveau contexte. La file d'attente survit à toutes les modifications de configuration car l'instance setRetain est définie sur true. La méthode runProtected s'exécute sur le thread d'interface utilisateur pour éviter une condition de concurrence avec l'indicateur isPaused.

public class PauseHandlerFragment extends Fragment {

    private AppCompatActivity context;
    private boolean isPaused = true;
    private Vector<MyRunnable> buffer = new Vector<>();

    @Override
    public void onAttach(Context context) {
        super.onAttach(context);
        this.context = (AppCompatActivity)context;
    }

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

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

    @Override
    public void onResume() {
        isPaused = false;
        playback();
        super.onResume();
    }

    private void playback() {
        while (buffer.size() > 0) {
            final MyRunnable runnable = buffer.elementAt(0);
            buffer.removeElementAt(0);
            new Handler(Looper.getMainLooper()).post(new Runnable() {
                @Override
                public void run() {
                    //execute run block, providing new context, incase 
                    //Android re-creates the parent activity
                    runnable.run(context);
                }
            });
        }
    }
    public final void runProtected(final MyRunnable runnable) {
        context.runOnUiThread(new Runnable() {
            @Override
            public void run() {
                if(isPaused) {
                    buffer.add(runnable);
                } else {
                    runnable.run(context);
                }
            }
        });
    }
}

Enfin, le fragment peut être utilisé dans une application principale comme suit:

public class SomeActivity extends AppCompatActivity implements SomeListener {
    PauseHandlerFragment mPauseHandlerFragment;

    static class Storyboard {
        public static String PAUSE_HANDLER_FRAGMENT_TAG = "phft";
    }

    protected void onCreate(Bundle savedInstanceState) {

        super.onCreate(savedInstanceState);

        ...

        //register pause handler 
        FragmentManager fm = getSupportFragmentManager();
        mPauseHandlerFragment = (PauseHandlerFragment) fm.
            findFragmentByTag(Storyboard.PAUSE_HANDLER_FRAGMENT_TAG);
        if(mPauseHandlerFragment == null) {
            mPauseHandlerFragment = new PauseHandlerFragment();
            fm.beginTransaction()
                .add(mPauseHandlerFragment, Storyboard.PAUSE_HANDLER_FRAGMENT_TAG)
                .commit();
        }

    }

    // part of SomeListener interface
    public void OnCallback(final String data) {
        mPauseHandlerFragment.runProtected(new MyRunnable() {
            @Override
            public void run(AppCompatActivity context) {
                //this block of code should be protected from IllegalStateException
                FragmentManager fm = context.getSupportFragmentManager();
                ...
            }
         });
    }
}
Rua109
la source
0

Dans mes projets, j'utilise le modèle de conception d'observateur pour résoudre ce problème. Dans Android, les récepteurs de diffusion et les intentions sont une implémentation de ce modèle.

Ce que je fais est de créer un BroadcastReceiver que j'inscrire dans du fragment de / activité onResume désinscriptions dans du fragment de / activité OnPause . Dans BroadcastReceiver méthode de OnReceive je mets tout le code qui a besoin pour fonctionner en raison de - la BroadcastReceiver - réception d' une intention (message) qui a été envoyé à votre application en général. Pour augmenter la sélectivité sur le type d'intents que votre fragment peut recevoir, vous pouvez utiliser un filtre d'intention comme dans l'exemple ci-dessous.

Un avantage de cette approche est que l' intention (message) peut être envoyée de partout dans votre application (une boîte de dialogue qui s'est ouverte au-dessus de votre fragment, une tâche asynchrone, un autre fragment, etc.). Les paramètres peuvent même être passés comme extras d'intention.

Un autre avantage est que cette approche est compatible avec n'importe quelle version d'API Android, puisque BroadcastReceivers et Intents ont été introduits au niveau d'API 1.

Vous n'êtes pas obligé de configurer des autorisations spéciales sur le fichier manifeste de votre application, sauf si vous prévoyez d'utiliser sendStickyBroadcast (où vous devez ajouter BROADCAST_STICKY).

public class MyFragment extends Fragment { 

    public static final String INTENT_FILTER = "gr.tasos.myfragment.refresh";

    private BroadcastReceiver mReceiver = new BroadcastReceiver() {

        // this always runs in UI Thread 
        @Override
        public void onReceive(Context context, Intent intent) {
            // your UI related code here

            // you can receiver data login with the intent as below
            boolean parameter = intent.getExtras().getBoolean("parameter");
        }
    };

    public void onResume() {
        super.onResume();
        getActivity().registerReceiver(mReceiver, new IntentFilter(INTENT_FILTER));

    };

    @Override
    public void onPause() {
        getActivity().unregisterReceiver(mReceiver);
        super.onPause();
    }

    // send a broadcast that will be "caught" once the receiver is up
    protected void notifyFragment() {
        Intent intent = new Intent(SelectCategoryFragment.INTENT_FILTER);
        // you can send data to receiver as intent extras
        intent.putExtra("parameter", true);
        getActivity().sendBroadcast(intent);
    }

}
dangel
la source
3
Si sendBroadcast () dans notifyFragment () est appelé pendant l'état Pause, unregisterReceiver () aura déjà été appelé et donc aucun récepteur ne sera là pour attraper cette intention. Le système Android ne rejettera-t-il pas l'intention s'il n'y a pas de code pour le gérer immédiatement?
Steve B
Je pense que les messages collants eventbus des robots verts sont comme ça, cool.
j2emanue