Come gestire i messaggi del gestore quando l'attività / frammento è in pausa


98

Leggera variazione sull'altro mio intervento

Fondamentalmente ho un messaggio Handlernel mio Fragmentche riceve una serie di messaggi che possono comportare la chiusura o la visualizzazione delle finestre di dialogo.

Quando l'app viene messa in background, ricevo un messaggio onPausema continuo a ricevere i miei messaggi come ci si aspetterebbe. Tuttavia, poiché sto usando i frammenti, non posso semplicemente chiudere e mostrare le finestre di dialogo poiché ciò risulterà in un file IllegalStateException.

Non posso semplicemente respingere o annullare l'autorizzazione alla perdita dello stato.

Dato che ho un Handlermi chiedo se esista un approccio consigliato su come gestire i messaggi mentre sono in uno stato di pausa.

Una possibile soluzione che sto prendendo in considerazione è registrare i messaggi in arrivo durante la pausa e riprodurli su un file onResume. Questo è in qualche modo insoddisfacente e penso che debba esserci qualcosa nel framework per gestirlo in modo più elegante.


1
potresti rimuovere tutti i messaggi nel gestore nel metodo onPause () del frammento, ma c'è un problema di ripristino dei messaggi che penso non sia possibile.
Yashwanth Kumar

Risposte:


167

Sebbene il sistema operativo Android non sembri avere un meccanismo che risolva sufficientemente il tuo problema, credo che questo modello fornisca una soluzione relativamente semplice da implementare.

La classe seguente è un wrapper attorno android.os.Handlerche memorizza nel buffer i messaggi quando un'attività viene sospesa e li riproduce alla ripresa.

Assicurati che qualsiasi codice di cui disponi che modifichi in modo asincrono uno stato di frammento (ad esempio commit, dismiss) sia chiamato solo da un messaggio nel gestore.

Deriva il tuo conduttore dalla PauseHandlerclasse.

Ogni volta che la tua attività riceve una onPause()chiamata PauseHandler.pause()e per onResume()chiamata PauseHandler.resume().

Sostituisci la tua implementazione del gestore handleMessage()con processMessage().

Fornire una semplice implementazione di storeMessage()cui ritorna sempre 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);
        }
    }
}

Di seguito è riportato un semplice esempio di come PausedHandlerpuò essere utilizzata la classe.

Con il clic di un pulsante viene inviato un messaggio ritardato al gestore.

Quando il gestore riceve il messaggio (sul thread dell'interfaccia utente) visualizza un file DialogFragment.

Se la PausedHandlerclasse non è stata utilizzata, verrà visualizzata un'eccezione IllegalStateException se il pulsante home è stato premuto dopo aver premuto il pulsante di prova per avviare la finestra di dialogo.

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

Ho aggiunto un storeMessage()metodo alla PausedHandlerclasse nel caso in cui i messaggi debbano essere elaborati immediatamente anche quando l'attività è sospesa. Se un messaggio viene gestito, deve essere restituito false e il messaggio verrà eliminato.


26
Bella soluzione, funziona a meraviglia. Non posso fare a meno di pensare che il framework dovrebbe gestire questo.
PJL

1
come passare la richiamata a DialogFragment?
Malachiasz

Non sono sicuro di aver capito la domanda Malachiasz, per favore potresti approfondire.
rinvio mcgraw

Questa è una soluzione molto elegante! A meno che non sbaglio, perché il resumemetodo utilizzato sendMessage(msg)tecnicamente potrebbero esserci altri thread che accodano il messaggio subito prima (o tra le iterazioni del ciclo), il che significa che i messaggi archiviati potrebbero essere intercalati con nuovi messaggi in arrivo. Non sono sicuro che sia un grosso problema. Forse l'uso sendMessageAtFrontOfQueue(e ovviamente l'iterazione all'indietro) risolverebbe questo problema?
yan

4
Penso che questo approccio potrebbe non funzionare sempre: se l'attività viene distrutta dal sistema operativo, l'elenco dei messaggi in attesa di essere elaborati sarà vuoto dopo la ripresa.
GaRRaPeTa

10

Una versione leggermente più semplice dell'eccellente PauseHandler del rinvio è

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

}

Presume che tu voglia sempre archiviare i messaggi offline per la riproduzione. E fornisce l'attività come input in #processMessagesmodo da non doverla gestire nella sottoclasse.


Perché i tuoi resume()e pause(), e handleMessage synchronized?
Maksim Dmitriev

5
Perché non vuoi che #pause venga chiamato durante #handleMessage e improvvisamente scopri che l'attività è nulla mentre la stai usando in #handleMessage. È una sincronizzazione attraverso lo stato condiviso.
William

@William Potresti spiegarmi per favore maggiori dettagli perché hai bisogno della sincronizzazione in una classe PauseHandler? Sembra che questa classe funzioni solo in un thread, thread dell'interfaccia utente. Immagino che #pause non possa essere chiamato durante #handleMessage perché entrambi funzionano nel thread dell'interfaccia utente.
Samik

@ William sei sicuro? HandlerThread handlerThread = new HandlerThread ("mHandlerNonMainThread"); handlerThread.start (); Looper looperNonMainThread = handlerThread.getLooper (); Handler handlerNonMainThread = new Handler (looperNonMainThread, new Callback () {public boolean handleMessage (Message msg) {return false;}});
swooby

Mi dispiace @swooby non seguo. Sono sicuro di cosa? E qual è lo scopo dello snippet di codice che hai pubblicato?
William

2

Ecco un modo leggermente diverso per affrontare il problema dell'esecuzione di commit di frammenti in una funzione di callback ed evitare il problema di IllegalStateException.

Per prima cosa crea un'interfaccia eseguibile personalizzata.

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

Successivamente, crea un frammento per l'elaborazione degli oggetti MyRunnable. Se l'oggetto MyRunnable è stato creato dopo che l'attività è stata messa in pausa, ad esempio se lo schermo viene ruotato o l'utente preme il pulsante home, viene messo in coda per una successiva elaborazione con un nuovo contesto. La coda sopravvive a qualsiasi modifica alla configurazione perché l'istanza setRetain è impostata su true. Il metodo runProtected viene eseguito sul thread dell'interfaccia utente per evitare una race condition con il flag 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);
                }
            }
        });
    }
}

Infine, il frammento può essere utilizzato in un'applicazione principale come segue:

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

0

Nei miei progetti utilizzo il design pattern dell'osservatore per risolvere questo problema. In Android, i ricevitori e gli intenti di trasmissione sono un'implementazione di questo modello.

Quello che faccio è creare un BroadcastReceiver che registro in fragment's / activity's onResume e annullare la registrazione in fragment's / activity's onPause . Nel metodo di BroadcastReceiver onReceive ho inserito tutto il codice che deve essere eseguito come risultato - del BroadcastReceiver - della ricezione di un Intent (messaggio) che è stato inviato alla tua app in generale. Per aumentare la selettività sul tipo di intenti che il tuo frammento può ricevere, puoi utilizzare un filtro di intenti come nell'esempio seguente.

Un vantaggio di questo approccio è che l' intento (messaggio) può essere inviato da qualsiasi luogo all'interno della tua app (una finestra di dialogo che si è aperta sopra il tuo frammento, un'attività asincrona, un altro frammento ecc.). I parametri possono anche essere passati come extra per intenti.

Un altro vantaggio è che questo approccio è compatibile con qualsiasi versione dell'API Android, poiché BroadcastReceivers e Intents sono stati introdotti al livello API 1.

Non è necessario impostare autorizzazioni speciali sul file manifest della tua app, tranne se intendi utilizzare sendStickyBroadcast (dove devi aggiungere 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);
    }

}

3
Se sendBroadcast () in notifyFragment () viene chiamato durante lo stato di Pausa, unregisterReceiver () sarà già stato chiamato e quindi nessun ricevitore sarà in giro per catturare quell'intento. Il sistema Android non eliminerà quindi l'intento se non è disponibile un codice per gestirlo immediatamente?
Steve B

Penso che i post appiccicosi dell'Eventbus dei robot verdi siano così, fantastico.
j2emanue
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.