Jak obsługiwać komunikaty obsługi, gdy aktywność / fragment jest wstrzymany

98

Niewielkie różnice w stosunku do mojego innego wpisu

Zasadniczo mam Handlerw moim komunikacie, Fragmentktóry odbiera kilka wiadomości, które mogą spowodować zamknięcie lub wyświetlenie okien dialogowych.

Kiedy aplikacja jest umieszczona w tle, otrzymuję komunikat, onPauseale nadal otrzymuję wiadomości zgodnie z oczekiwaniami. Jednak ponieważ używam fragmentów, nie mogę po prostu odrzucić i pokazać okien dialogowych, ponieważ spowoduje to, że plik IllegalStateException.

Nie mogę po prostu odrzucić ani anulować zezwolenia na utratę stanu.

Biorąc pod uwagę, że mam a Handler, zastanawiam się, czy istnieje zalecane podejście do tego, jak powinienem obsługiwać wiadomości w stanie wstrzymania.

Jednym z możliwych rozwiązań, które rozważam, jest nagrywanie wiadomości przychodzących podczas wstrzymania i odtwarzanie ich w formacie onResume. Jest to trochę niezadowalające i myślę, że musi być coś w ramach, aby poradzić sobie z tym bardziej elegancko.

PJL
źródło
1
można usunąć wszystkie komunikaty w module obsługi w metodzie onPause () fragmentu, ale jest problem z przywróceniem komunikatów, co moim zdaniem nie jest możliwe.
Yashwanth Kumar

Odpowiedzi:

167

Chociaż wydaje się, że system operacyjny Android nie ma mechanizmu, który dostatecznie rozwiązuje Twój problem, uważam, że ten wzorzec zapewnia stosunkowo proste do zaimplementowania obejście.

Poniższa klasa jest opakowaniem, android.os.Handlerktóre buforuje wiadomości, gdy działanie jest wstrzymane i odtwarza je po wznowieniu.

Upewnij się, że każdy posiadany kod, który asynchronicznie zmienia stan fragmentu (np. Zatwierdzenie, odrzucenie), jest wywoływany tylko z komunikatu w module obsługi.

Wyprowadź swojego handlera z PauseHandlerklasy.

Zawsze, gdy Twoja aktywność odbiera onPause()telefon PauseHandler.pause()i onResume()dzwoni PauseHandler.resume().

Wymień realizację Handler handleMessage()zprocessMessage() .

Zapewnij prostą implementację, storeMessage()która zawsze wraca 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);
        }
    }
}

Poniżej znajduje się prosty przykład wykorzystania tej PausedHandlerklasy.

Kliknięcie przycisku powoduje wysłanie opóźnionej wiadomości do programu obsługi.

Gdy program obsługi odbiera komunikat (w wątku interfejsu użytkownika), wyświetla plik DialogFragment.

Jeśli PausedHandlerklasa nie była używana, wyjątek IllegalStateException byłby wyświetlany, jeśli przycisk strony głównej został naciśnięty po naciśnięciu przycisku testowego w celu uruchomienia okna dialogowego.

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

Dodałem storeMessage()do PausedHandlerklasy metodę na wypadek, gdyby jakiekolwiek wiadomości były przetwarzane natychmiast, nawet gdy działanie jest wstrzymane. Jeśli wiadomość jest obsługiwana, należy zwrócić false, a wiadomość zostanie odrzucona.

quickdraw mcgraw
źródło
26
Niezłe rozwiązanie, działa wspaniale. Nie mogę przestać myśleć, że ramy powinny to obsługiwać.
PJL
1
jak przekazać wywołanie zwrotne do DialogFragment?
Malachiasz
Nie jestem pewien, czy rozumiem pytanie Malachiasz, czy mógłbyś to rozwinąć.
ekspresowe mcgraw
To bardzo eleganckie rozwiązanie! O ile się nie mylę, ponieważ resumemetoda wykorzystuje sendMessage(msg)technicznie, mogą istnieć inne wątki kolejkujące wiadomość tuż przed (lub pomiędzy iteracjami pętli), co oznacza, że ​​przechowywane wiadomości mogą być przeplatane nowymi wiadomościami. Nie jestem pewien, czy to wielka sprawa. Może użycie sendMessageAtFrontOfQueue(i oczywiście iteracja wstecz) rozwiązałoby ten problem?
yan
4
Myślę, że to podejście może nie zawsze działać - jeśli aktywność zostanie zniszczona przez system operacyjny, lista wiadomości oczekujących na procesy będzie pusta po wznowieniu.
GaRRaPeTa
10

Nieco prostszą wersją doskonałego PauseHandler ekspresu jest

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

}

Zakłada się, że zawsze chcesz przechowywać wiadomości offline do ponownego odtworzenia. I zapewnia działanie jako dane wejściowe, #processMessageswięc nie musisz zarządzać nim w podklasie.

William
źródło
Dlaczego Twoje resume()i pause(), i handleMessage synchronized?
Maksim Dmitriev
5
Ponieważ nie chcesz, aby #pause była wywoływana podczas #handleMessage i nagle odkryjesz, że aktywność jest zerowa, gdy używasz jej w #handleMessage. To synchronizacja w stanie udostępnionym.
William
@William Czy możesz mi wyjaśnić więcej szczegółów, dlaczego potrzebujesz synchronizacji w klasie PauseHandler? Wygląda na to, że ta klasa działa tylko w jednym wątku, wątku interfejsu użytkownika. Wydaje mi się, że #pause nie mogło zostać wywołane podczas #handleMessage, ponieważ oba działają w wątku UI.
Samik
@William czy na pewno? 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
Przepraszam @swooby, nie śledzę. Czy jestem pewien czego? Jaki jest cel opublikowanego przez Ciebie fragmentu kodu?
William
2

Oto nieco inny sposób podejścia do problemu wykonywania zatwierdzeń fragmentów w funkcji wywołania zwrotnego i unikania problemu IllegalStateException.

Najpierw utwórz niestandardowy interfejs, który można uruchomić.

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

Następnie utwórz fragment do przetwarzania obiektów MyRunnable. Jeśli obiekt MyRunnable został utworzony po wstrzymaniu działania, np. W przypadku obrócenia ekranu lub naciśnięcia przycisku głównego przez użytkownika, jest on umieszczany w kolejce do późniejszego przetwarzania z nowym kontekstem. Kolejka przetrwa wszelkie zmiany konfiguracji, ponieważ instancja setRetain ma wartość true. Metoda runProtected działa w wątku interfejsu użytkownika, aby uniknąć sytuacji wyścigu z 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);
                }
            }
        });
    }
}

Wreszcie fragment można wykorzystać w głównej aplikacji w następujący sposób:

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
źródło
0

W moich projektach używam wzorca projektowego obserwatora, aby rozwiązać ten problem. W systemie Android odbiorniki i intencje transmisji są realizacją tego wzorca.

Co mogę zrobić, to stworzyć BroadcastReceiver który zarejestrować się fragment w / działalności za onResume i wyrejestrowywanie w fragment w / działalności za OnPause . W metodzie BroadcastReceiver onReceive umieściłem cały kod, który musi zostać uruchomiony w wyniku - BroadcastReceiver - odebrania intencji (wiadomości), która została wysłana do Twojej aplikacji. Aby zwiększyć selektywność co do typu intencji, jaki może otrzymać Twój fragment, możesz użyć filtra intencji jak w poniższym przykładzie.

Zaletą tego podejścia jest to, że intencja (wiadomość) może być wysyłana z dowolnego miejsca w aplikacji (okno dialogowe, które otwiera się na górze Twojego fragmentu, zadanie asynchroniczne, inny fragment itp.). Parametry mogą być nawet przekazywane jako dodatki intencyjne.

Kolejną zaletą jest to, że to podejście jest kompatybilne z każdą wersją interfejsu API Androida, ponieważ BroadcastReceivers i Intents zostały wprowadzone na poziomie API 1.

Nie musisz ustawiać żadnych specjalnych uprawnień w pliku manifestu swojej aplikacji, chyba że planujesz używać sendStickyBroadcast (gdzie musisz dodać 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
źródło
3
Jeśli sendBroadcast () w notifyFragment () jest wywoływana w stanie wstrzymania, funkcja unregisterReceiver () została już wywołana, a zatem żaden odbiornik nie będzie w pobliżu, aby przechwycić tę intencję. Czy system Android nie odrzuci zamiaru, jeśli nie ma kodu, który natychmiast go obsłuży?
Steve B
Myślę, że przyklejone posty z zielonych robotów eventbus są takie, super.
j2emanue