Como lidar com mensagens do manipulador quando a atividade / fragmento é pausado

98

Ligeira variação na minha outra postagem

Basicamente, tenho uma mensagem Handlerna minha Fragmentque recebe um monte de mensagens que podem resultar em caixas de diálogo sendo descartadas ou exibidas.

Quando o aplicativo é colocado em segundo plano, recebo um, onPausemas ainda recebo minhas mensagens como esperado. No entanto, como estou usando fragmentos, não posso simplesmente descartar e mostrar os diálogos, pois isso resultará em um IllegalStateException.

Não posso simplesmente dispensar ou cancelar a perda de estado.

Visto que tenho um Handler, estou me perguntando se há uma abordagem recomendada sobre como devo lidar com as mensagens em um estado de pausa.

Uma possível solução que estou considerando é gravar as mensagens que chegam durante a pausa e reproduzi-las em um onResume. Isso é um tanto insatisfatório e estou pensando que deve haver algo na estrutura para lidar com isso de forma mais elegante.

PJL
fonte
1
você poderia remover todas as mensagens no manipulador no método onPause () do fragmento, mas há um problema de restauração das mensagens que eu acho que não é possível.
Yashwanth Kumar

Respostas:

167

Embora o sistema operacional Android não pareça ter um mecanismo que resolva suficientemente o seu problema, acredito que esse padrão oferece uma solução alternativa relativamente simples de implementar.

A classe a seguir é um wrapper android.os.Handlerque armazena mensagens quando uma atividade é pausada e as reproduz na continuação.

Certifique-se de que qualquer código que você tenha, que muda de forma assíncrona um estado de fragmento (por exemplo, confirmar, rejeitar), seja chamado apenas de uma mensagem no manipulador.

Derive seu manipulador da PauseHandlerclasse.

Sempre que sua atividade recebe uma onPause()chamada PauseHandler.pause()e para onResume()chamada PauseHandler.resume().

Substitua sua implementação do Handler handleMessage()por processMessage().

Fornece uma implementação simples da storeMessage()qual sempre retorna 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);
        }
    }
}

Abaixo está um exemplo simples de como a PausedHandlerclasse pode ser usada.

Com o clique de um botão, uma mensagem atrasada é enviada ao manipulador.

Quando o manipulador recebe a mensagem (no thread de IU), ele exibe um DialogFragment.

Se a PausedHandlerclasse não estivesse sendo usada, uma IllegalStateException seria mostrada se o botão home fosse pressionado após pressionar o botão de teste para iniciar o 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;
                }
            }
        }
    }
}

Eu adicionei um storeMessage()método à PausedHandlerclasse no caso de qualquer mensagem deve ser processada imediatamente, mesmo quando a atividade está pausada. Se uma mensagem for tratada, false deve ser retornado e a mensagem será descartada.

desenho rápido mcgraw
fonte
26
Boa solução, funciona muito bem. Não posso deixar de pensar, entretanto, que a estrutura deve lidar com isso.
PJL
1
como passar callback para DialogFragment?
Malachiasz de
Não tenho certeza se entendi a pergunta Malachiasz, por favor, você poderia explicar.
quickdraw mcgraw
Esta é uma solução muito elegante! A menos que eu esteja errado, porque o resumemétodo usa sendMessage(msg)tecnicamente, pode haver outros threads enfileirando a mensagem logo antes (ou entre as iterações do loop), o que significa que as mensagens armazenadas podem ser intercaladas com a chegada de novas mensagens. Não tenho certeza se é um grande negócio. Talvez usar sendMessageAtFrontOfQueue(e é claro iterar para trás) resolveria esse problema?
yan
4
Acho que essa abordagem pode nem sempre funcionar - se a atividade for destruída pelo sistema operacional, a lista de mensagens pendentes para serem processos ficará vazia após a retomada.
GaRRaPeTa
10

Uma versão um pouco mais simples do excelente PauseHandler do quickdraw é

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

}

Ele supõe que você sempre deseja armazenar mensagens offline para reprodução. E fornece a Activity como entrada para #processMessagesque você não precise gerenciá-la na subclasse.

William
fonte
Por que são seus resume()e pause(), e handleMessage synchronized?
Maksim Dmitriev
5
Porque você não quer que #pause seja chamado durante #handleMessage e de repente descubra que a atividade é nula enquanto você a está usando em #handleMessage. É uma sincronização em estado compartilhado.
William
@William Você poderia me explicar mais detalhes por que você precisa de sincronização em uma classe PauseHandler? Parece que esta classe funciona apenas em um thread, UI thread. Eu acho que #pause não pode ser chamado durante #handleMessage porque ambos funcionam no thread de IU.
Samik
@William tem certeza? 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
Desculpe @swooby, eu não entendo. Tenho certeza de quê? E qual é o propósito do snippet de código que você postou?
William
2

Aqui está uma maneira ligeiramente diferente de abordar o problema de fazer commits de Fragment em uma função de retorno de chamada e evitar o problema IllegalStateException.

Primeiro crie uma interface executável personalizada.

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

Em seguida, crie um fragmento para processar os objetos MyRunnable. Se o objeto MyRunnable foi criado depois que a Activity foi pausada, por exemplo, se a tela foi girada, ou o usuário pressionou o botão home, ele é colocado em uma fila para processamento posterior com um novo contexto. A fila sobrevive a quaisquer alterações de configuração porque a instância setRetain está definida como verdadeira. O método runProtected é executado no thread da IU para evitar uma condição de corrida com o sinalizador 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, o fragmento pode ser usado em um aplicativo principal da seguinte maneira:

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

Em meus projetos, uso o padrão de design do observador para resolver isso. No Android, broadcast receivers e intents são uma implementação desse padrão.

O que eu faço é criar um BroadcastReceiver que me registrar no compartimento do fragmento / atividade onResume e cancelar o registro no compartimento do fragmento / atividade onPause . Em BroadcastReceiver método de OnReceive eu coloquei todo o código que precisa ser executado como resultado de - a BroadcastReceiver - receber uma Intenção (mensagem) que foi enviado para o seu aplicativo em geral. Para aumentar a seletividade sobre o tipo de intents que seu fragmento pode receber, você pode usar um filtro de intent como no exemplo abaixo.

Uma vantagem dessa abordagem é que o Intent (mensagem) pode ser enviado de qualquer lugar dentro do seu aplicativo (uma caixa de diálogo aberta no topo do seu fragmento, uma tarefa assíncrona, outro fragmento etc.). Os parâmetros podem até ser passados ​​como extras de intent.

Outra vantagem é que essa abordagem é compatível com qualquer versão da API do Android, uma vez que BroadcastReceivers e Intents foram introduzidos na API de nível 1.

Você não é obrigado a configurar nenhuma permissão especial no arquivo de manifesto do seu aplicativo, exceto se você planeja usar sendStickyBroadcast (onde você precisa adicionar 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
fonte
3
Se sendBroadcast () em notificarFragment () for chamado durante o estado de pausa, unregisterReceiver () já terá sido chamado e, portanto, nenhum receptor estará por perto para capturar esse intent. O sistema Android não descartará a intenção se não houver código para tratá-la imediatamente?
Steve B
Eu acho que as postagens pegajosas do eventbus de robôs verdes são assim, legal.
j2emanue