Cómo manejar los mensajes de Handler cuando la actividad / fragmento está en pausa

98

Ligera variación en mi otra publicación

Básicamente, tengo un mensaje Handleren mi Fragmentque recibe un montón de mensajes que pueden hacer que los diálogos se descarten o se muestren.

Cuando la aplicación se coloca en segundo plano, obtengo un onPausemensaje, pero aún así recibo mis mensajes como era de esperar. Sin embargo, debido a que estoy usando fragmentos, no puedo simplemente descartar y mostrar diálogos, ya que eso dará como resultado un archivo IllegalStateException.

No puedo simplemente descartar o cancelar permitiendo la pérdida del estado.

Dado que tengo un Handler, me pregunto si existe un enfoque recomendado sobre cómo debo manejar los mensajes mientras estoy en pausa.

Una posible solución que estoy considerando es grabar los mensajes que llegan mientras están en pausa y reproducirlos en formato onResume. Esto es algo insatisfactorio y estoy pensando que debe haber algo en el marco para manejar esto de manera más elegante.

PJL
fuente
1
podría eliminar todos los mensajes en el controlador en el método de fragmento onPause (), pero hay un problema de restaurar los mensajes que creo que no es posible.
Yashwanth Kumar

Respuestas:

167

Aunque el sistema operativo Android no parece tener un mecanismo que aborde suficientemente su problema, creo que este patrón proporciona una solución relativamente simple de implementar.

La siguiente clase es un contenedor android.os.Handlerque almacena los mensajes cuando una actividad está en pausa y los reproduce en el currículum.

Asegúrese de que cualquier código que tenga que cambie de forma asincrónica el estado de un fragmento (por ejemplo, confirmar, descartar) solo se llame desde un mensaje en el controlador.

Derive a su controlador de la PauseHandlerclase.

Siempre que su actividad reciba una onPause()llamada PauseHandler.pause()y para onResume()llamar PauseHandler.resume().

Reemplace su implementación del Handler handleMessage()con processMessage().

Proporcionar una implementación sencilla de la storeMessage()que siempre regrese 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);
        }
    }
}

A continuación se muestra un ejemplo simple de cómo PausedHandlerse puede usar la clase.

Al hacer clic en un botón, se envía un mensaje retrasado al administrador.

Cuando el controlador recibe el mensaje (en el hilo de la interfaz de usuario), muestra un DialogFragment.

Si la PausedHandlerclase no se estaba utilizando, se mostraría una IllegalStateException si se presionaba el botón de inicio después de presionar el botón de prueba para iniciar el cuadro de diálogo.

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

Agregué un storeMessage()método a la PausedHandlerclase en caso de que algún mensaje deba procesarse inmediatamente incluso cuando la actividad está en pausa. Si se maneja un mensaje, se debe devolver falso y el mensaje se descartará.

Quickdraw mcgraw
fuente
26
Buena solución, funciona de maravilla. Sin embargo, no puedo evitar pensar que el marco debería manejar esto.
PJL
1
¿Cómo pasar la devolución de llamada a DialogFragment?
Malachiasz
No estoy seguro de haber entendido la pregunta Malachiasz, por favor, ¿podría explicarme?
Quickdraw mcgraw
¡Esta es una solución muy elegante! A menos que me equivoque, debido a que el resumemétodo utiliza sendMessage(msg)técnicamente, podría haber otros subprocesos que pongan en cola el mensaje justo antes (o entre iteraciones del bucle), lo que significa que los mensajes almacenados podrían intercalarse con la llegada de nuevos mensajes. No estoy seguro de si es un gran problema. ¿Quizás usar sendMessageAtFrontOfQueue(y por supuesto iterar hacia atrás) resolvería este problema?
yan
4
Creo que este enfoque no siempre funciona: si el sistema operativo destruye la actividad, la lista de mensajes pendientes de ser procesos estará vacía después de la reanudación.
GaRRaPeTa
10

Una versión ligeramente más simple del excelente PauseHandler de quickdraw es

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

}

Se asume que siempre desea almacenar mensajes sin conexión para reproducirlos. Y proporciona la Actividad como entrada para #processMessagesque no necesite administrarla en la subclase.

Guillermo
fuente
¿Por qué son tu resume()y pause(), y handleMessage synchronized?
Maksim Dmitriev
5
Porque no desea que se llame a #pause durante #handleMessage y de repente descubra que la actividad es nula mientras la usa en #handleMessage. Es una sincronización entre estados compartidos.
William
@William ¿Podría explicarme más detalles por qué necesita la sincronización en una clase PauseHandler? Parece que esta clase solo funciona en un hilo, el hilo de la interfaz de usuario. Supongo que no se pudo llamar a #pause durante #handleMessage porque ambos funcionan en el hilo de la interfaz de usuario.
Samik
@William, ¿estás seguro? 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
Lo siento @swooby, no te sigo. ¿Estoy seguro de qué? ¿Y cuál es el propósito del fragmento de código que publicó?
William
2

Aquí hay una forma ligeramente diferente de abordar el problema de realizar confirmaciones de Fragmento en una función de devolución de llamada y evitar el problema IllegalStateException.

Primero cree una interfaz ejecutable personalizada.

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

A continuación, cree un fragmento para procesar los objetos MyRunnable. Si el objeto MyRunnable se creó después de que se pausó la actividad, por ejemplo, si se gira la pantalla o el usuario presiona el botón de inicio, se coloca en una cola para procesarlo posteriormente con un nuevo contexto. La cola sobrevive a cualquier cambio de configuración porque la instancia de setRetain se establece en true. El método runProtected se ejecuta en el subproceso de la interfaz de usuario para evitar una condición de carrera con el indicador 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);
                }
            }
        });
    }
}

Finalmente, el fragmento se puede usar en una aplicación principal de la siguiente manera:

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
fuente
0

En mis proyectos, uso el patrón de diseño del observador para resolver esto. En Android, los receptores de difusión y las intenciones son una implementación de este patrón.

Lo que hago es crear un BroadcastReceiver la que me registro en un fragmento de la / actividad onResume y anular el registro en el fragmento de de / actividad onPause . En BroadcastReceiver método 's OnReceive pongo todo el código que necesitan ser ejecutadas como resultado de - el BroadcastReceiver - recibir una Intención (mensaje) que es enviada a su aplicación en general. Para aumentar la selectividad sobre qué tipo de intenciones puede recibir su fragmento, puede usar un filtro de intenciones como en el ejemplo siguiente.

Una ventaja de este enfoque es que el Intent (mensaje) se puede enviar desde cualquier lugar dentro de su aplicación (un diálogo que se abrió encima de su fragmento, una tarea asíncrona, otro fragmento, etc.). Los parámetros pueden incluso pasar como extras de intención.

Otra ventaja es que este enfoque es compatible con cualquier versión de API de Android, ya que BroadcastReceivers e Intents se han introducido en el nivel de API 1.

No es necesario que configure ningún permiso especial en el archivo de manifiesto de su aplicación, excepto si planea usar sendStickyBroadcast (donde necesita agregar 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);
    }

}
peligroso
fuente
3
Si se llama a sendBroadcast () en notifyFragment () durante el estado de pausa, ya se habrá llamado a unregisterReceiver () y, por lo tanto, no habrá ningún receptor para detectar esa intención. ¿No descartará el sistema Android la intención si no hay un código para manejarla de inmediato?
Steve B
Creo que las publicaciones adhesivas de eventbus de robots verdes son así, genial.
j2emanue