Umgang mit Handler-Nachrichten, wenn Aktivität / Fragment angehalten wird

98

Leichte Abweichung von meinem anderen Beitrag

Grundsätzlich habe ich eine Nachricht Handlerin meinem, Fragmentdie eine Reihe von Nachrichten empfängt, die dazu führen können, dass Dialoge geschlossen oder angezeigt werden.

Wenn die App in den Hintergrund gestellt wird, erhalte ich eine, onPauseaber dann kommen meine Nachrichten immer noch wie erwartet durch. Da ich jedoch Fragmente verwende, kann ich Dialoge nicht einfach schließen und anzeigen, da dies zu einem führt IllegalStateException.

Ich kann nicht einfach entlassen oder stornieren, um einen Staatsverlust zuzulassen.

Angesichts der Tatsache, dass ich eine Handlerhabe, frage ich mich, ob es einen empfohlenen Ansatz gibt, wie ich mit Nachrichten in einem angehaltenen Zustand umgehen soll.

Eine mögliche Lösung, die ich in Betracht ziehe, besteht darin, die eingehenden Nachrichten während der Pause aufzuzeichnen und sie auf einem abzuspielen onResume. Dies ist etwas unbefriedigend und ich denke, dass es etwas im Rahmen geben muss, um dies eleganter zu handhaben.

PJL
quelle
1
Sie könnten alle Nachrichten im Handler in der onPause () -Methode des Fragments entfernen, aber es gibt ein Problem beim Wiederherstellen der Nachrichten, das meiner Meinung nach nicht möglich ist.
Yashwanth Kumar

Antworten:

167

Obwohl das Android-Betriebssystem anscheinend keinen Mechanismus hat, der Ihr Problem ausreichend behebt, glaube ich, dass dieses Muster eine relativ einfach zu implementierende Problemumgehung bietet.

Die folgende Klasse ist ein Wrapper android.os.Handler, der Nachrichten puffert, wenn eine Aktivität angehalten wird, und sie bei der Wiedergabe wiedergibt.

Stellen Sie sicher, dass jeder Code, der einen Fragmentstatus asynchron ändert (z. B. Festschreiben, Entlassen), nur von einer Nachricht im Handler aufgerufen wird.

Leiten Sie Ihren Handler aus der PauseHandlerKlasse ab.

Wann immer Ihre Aktivität einen onPause()Anruf erhält PauseHandler.pause()und zum onResume()Anruf PauseHandler.resume().

Ersetzen Sie Ihre Implementierung des Handlers handleMessage()durch processMessage().

Stellen Sie eine einfache Implementierung bereit, storeMessage()die immer zurückkehrt 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);
        }
    }
}

Im Folgenden finden Sie ein einfaches Beispiel für die Verwendung der PausedHandlerKlasse.

Auf Knopfdruck wird eine verzögerte Nachricht an den Handler gesendet.

Wenn der Handler die Nachricht empfängt (im UI-Thread), wird a angezeigt DialogFragment.

Wenn die PausedHandlerKlasse nicht verwendet wurde, wird eine IllegalStateException angezeigt, wenn die Home-Taste nach dem Drücken der Test-Taste gedrückt wurde, um den Dialog zu starten.

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

Ich habe storeMessage()der PausedHandlerKlasse eine Methode hinzugefügt, falls Nachrichten sofort verarbeitet werden sollen, auch wenn die Aktivität angehalten wird. Wenn eine Nachricht behandelt wird, sollte false zurückgegeben werden und die Nachricht wird verworfen.

Quickdraw mcgraw
quelle
26
Schöne Lösung, funktioniert ein Genuss. Ich kann nicht anders, als zu denken, dass das Framework dies handhaben sollte.
PJL
1
Wie kann ich einen Rückruf an DialogFragment weiterleiten?
Malachiasz
Ich bin mir nicht sicher, ob ich die Frage Malachiasz verstehe. Könnten Sie das bitte näher erläutern?
Quickdraw Mcgraw
Dies ist eine sehr elegante Lösung! Es sei denn, ich irre mich, da die resumeMethode sendMessage(msg)technisch verwendet, können andere Threads unmittelbar vor (oder zwischen den Iterationen der Schleife) Nachrichten in die Warteschlange stellen, was bedeutet, dass die gespeicherten Nachrichten mit neuen Nachrichten verschachtelt werden können. Ich bin mir nicht sicher, ob es eine große Sache ist. Vielleicht würde die Verwendung sendMessageAtFrontOfQueue(und natürlich das Rückwärtslaufen) dieses Problem lösen?
yan
4
Ich denke, dieser Ansatz funktioniert möglicherweise nicht immer. Wenn die Aktivität vom Betriebssystem zerstört wird, ist die Liste der Nachrichten, deren Prozesse noch ausstehen, nach der Wiederaufnahme leer.
GaRRaPeTa
10

Eine etwas einfachere Version von Quickdraws ausgezeichnetem PauseHandler ist

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

}

Es wird davon ausgegangen, dass Sie immer Offline-Nachrichten zur Wiedergabe speichern möchten. Und stellt die Aktivität als Eingabe bereit, #processMessagesdamit Sie sie nicht in der Unterklasse verwalten müssen.

Wilhelm
quelle
Warum sind deine resume()und pause()und handleMessage synchronized?
Maksim Dmitriev
5
Weil Sie nicht möchten, dass #pause während #handleMessage aufgerufen wird und plötzlich feststellen, dass die Aktivität null ist, während Sie sie in #handleMessage verwenden. Es ist eine Synchronisation zwischen gemeinsam genutzten Status.
William
@William Könnten Sie mir bitte näher erläutern, warum Sie eine Synchronisation in einer PauseHandler-Klasse benötigen? Es scheint, dass diese Klasse nur in einem Thread funktioniert, dem UI-Thread. Ich denke, dass #pause während #handleMessage nicht aufgerufen werden konnte, da beide im UI-Thread funktionieren.
Samik
@ William bist du sicher? HandlerThread handlerThread = neuer HandlerThread ("mHandlerNonMainThread"); handlerThread.start (); Looper looperNonMainThread = handlerThread.getLooper (); Handler handlerNonMainThread = neuer Handler (looperNonMainThread, neuer Callback () {public boolean handleMessage (Message msg) {return false;}});
Swooby
Sorry @swooby, ich folge nicht. Bin ich mir sicher was? Und was ist der Zweck des von Ihnen geposteten Code-Snippets?
William
2

Hier ist eine etwas andere Methode, um das Problem der Ausführung von Fragment-Commits in einer Rückruffunktion und der Vermeidung des IllegalStateException-Problems anzugehen.

Erstellen Sie zunächst eine benutzerdefinierte ausführbare Schnittstelle.

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

Erstellen Sie als Nächstes ein Fragment für die Verarbeitung der MyRunnable-Objekte. Wenn das MyRunnable-Objekt nach dem Anhalten der Aktivität erstellt wurde, z. B. wenn der Bildschirm gedreht wird oder der Benutzer die Home-Taste drückt, wird es zur späteren Verarbeitung mit einem neuen Kontext in eine Warteschlange gestellt. Die Warteschlange überlebt alle Konfigurationsänderungen, da die setRetain-Instanz auf true gesetzt ist. Die Methode runProtected wird im UI-Thread ausgeführt, um eine Racebedingung mit dem Flag isPaused zu vermeiden.

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

Schließlich kann das Fragment in einer Hauptanwendung wie folgt verwendet werden:

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

In meinen Projekten verwende ich das Beobachter-Entwurfsmuster, um dies zu lösen. In Android sind Rundfunkempfänger und -absichten eine Implementierung dieses Musters.

Ich erstelle einen BroadcastReceiver, den ich in onResume von fragment / activity registriere und in onPause von fragment / activity abhebe . In der BroadcastReceiver -Methode onReceive habe ich den gesamten Code eingefügt , der ausgeführt werden muss, wenn - der BroadcastReceiver - eine Absicht (Nachricht) empfangen hat, die im Allgemeinen an Ihre App gesendet wurde. Um die Selektivität für die Art der Absichten zu erhöhen, die Ihr Fragment empfangen kann, können Sie einen Absichtsfilter wie im folgenden Beispiel verwenden.

Ein Vorteil dieses Ansatzes ist, dass die Absicht (Nachricht) von überall in Ihrer App gesendet werden kann (ein Dialogfeld, das über Ihrem Fragment geöffnet wurde, eine asynchrone Aufgabe, ein anderes Fragment usw.). Parameter können sogar als Absichts-Extras übergeben werden.

Ein weiterer Vorteil ist, dass dieser Ansatz mit jeder Android-API-Version kompatibel ist, da BroadcastReceivers und Intents auf API-Ebene 1 eingeführt wurden.

Sie müssen keine speziellen Berechtigungen für die Manifestdatei Ihrer App einrichten, es sei denn, Sie möchten sendStickyBroadcast verwenden (wo Sie BROADCAST_STICKY hinzufügen müssen).

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
quelle
3
Wenn sendBroadcast () in notifyFragment () während des Pausenzustands aufgerufen wird, wurde unregisterReceiver () bereits aufgerufen, und daher ist kein Empfänger in der Nähe, um diese Absicht zu erfassen. Wird das Android-System dann nicht die Absicht verwerfen, wenn es keinen Code gibt, der sofort damit umgehen kann?
Steve B
Ich denke, grüne Roboter Eventbus Sticky Posts sind so, cool.
j2emanue