Як обробляти повідомлення обробника, коли діяльність / фрагмент призупинено


98

Незначна варіація моєї іншої публікації

В принципі у мене є повідомлення Handlerв моєму Fragmentяка отримує купу повідомлень , які можуть привести до діалогів звільняють або показано на малюнку.

Коли додаток перебуває у фоновому режимі, я отримую, onPauseале все одно отримую повідомлення, як можна було б очікувати. Однак, оскільки я використовую фрагменти, я не можу просто відхиляти та показувати діалоги, оскільки це призведе до появи IllegalStateException.

Я не можу просто звільнити чи скасувати дозволу втрати держави.

З огляду на те, що у мене є питання, Handlerмені цікаво, чи є рекомендований підхід до того, як я повинен обробляти повідомлення, перебуваючи в паузі.

Одне з можливих рішень, які я розглядаю, - це записати повідомлення, які надходять під час призупинення, і відтворити їх назад на onResume. Це дещо незадовільно, і я думаю, що повинно бути щось в рамках, щоб вирішити це більш елегантно.


1
Ви можете видалити всі повідомлення з обробника у фрагменті методу onPause (), але існує проблема відновлення повідомлень, які, на мою думку, неможливі.
Яшувант Кумар

Відповіді:


167

Незважаючи на те, що в операційній системі Android не існує механізму, який достатньо вирішує вашу проблему, я вважаю, що ця модель забезпечує досить простий спосіб втілення.

Наступний клас - це обгортка, android.os.Handlerяка зберігає повідомлення, коли діяльність призупинена, і відтворює їх при резюме.

Переконайтеся, що будь-який код, який у вас асинхронно змінює стан фрагмента (наприклад, виконувати, відхиляти), викликається лише з повідомлення в обробнику.

Отримайте свого обробника з PauseHandlerкласу.

Щоразу, коли ваша діяльність отримує onPause()дзвінок PauseHandler.pause()і onResume()дзвінок PauseHandler.resume().

Замініть свою реалізацію Обробника handleMessage()на processMessage().

Надайте просту реалізацію, storeMessage()яка завжди повертається 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);
        }
    }
}

Нижче наводиться простий приклад того, як PausedHandlerможна використовувати клас.

Після натискання кнопки відкладене повідомлення надсилається оброблювачу.

Коли обробник отримує повідомлення (в потоці інтерфейсу користувача), воно відображає a DialogFragment.

Якщо PausedHandlerклас не використовувався, буде показано IllegalStateException, якщо натиснути кнопку home після натискання кнопки тесту для запуску діалогового вікна.

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

Я додав storeMessage()метод до PausedHandlerкласу у випадку, якщо будь-які повідомлення слід негайно обробити, навіть коли діяльність призупинена. Якщо повідомлення обробляється, то слід повернути помилкове, і повідомлення буде відкинуто.


26
Приємне рішення, працює частування. Не можу не думати, хоча рамки повинні вирішувати це.
PJL

1
як передати зворотний дзвінок DialogFragment?
Малахіаш

Я не впевнений, що я розумію питання Малахіаш.
quickdraw mcgraw

Це дуже елегантне рішення! Якщо я не помиляюся, тому що resumeметод sendMessage(msg)технічно використовує, можуть бути інші потоки, що передають повідомлення безпосередньо перед цим (або між ітераціями циклу), а це означає, що збережені повідомлення можуть бути переплетені з новими повідомленнями, що надходять. Не впевнений, чи велика справа. Може, використання sendMessageAtFrontOfQueue(і, звичайно, повторення назад) вирішило б це питання?
янь

4
Я думаю, що такий підхід може не завжди працювати - якщо діяльність буде знищена ОС, список повідомлень, що очікують на процес, буде порожнім після відновлення.
GaRRaPeTa

10

Трохи простішою версією відмінного PauseHandler є 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);

}

Це передбачає, що ви завжди хочете зберігати офлайн-повідомлення для повторного відтворення. І надає Діяльність як вхід, щоб #processMessagesвам не потрібно керувати ним у підкласі.


Чому ваші resume()і pause(), і handleMessage synchronized?
Максим Дмитрієв

5
Тому що ви не хочете, щоб #pause викликали під час #handleMessage і раптом виявите, що ця активність є нульовою, коли ви використовуєте її в #handleMessage. Це синхронізація в загальному стані.
Вільям

@William Ви можете мені пояснити, будь ласка, детальніше, чому вам потрібна синхронізація в класі PauseHandler? Здається, що цей клас працює лише в одному потоці, потоці UI. Я думаю, що #pause не вдалося викликати під час #handleMessage, оскільки вони працюють у потоці користувальницького інтерфейсу.
Самік

@William ви впевнені? HandlerThread handlerThread = новий HandlerThread ("mHandlerNonMainThread"); handlerThread.start (); Looper looperNonMainThread = handlerThread.getLooper (); Handler handlerNonMainThread = новий Handler (looperNonMainThread, новий callback () {public boolean handleMessage (повідомлення msg) {return false;}});
swooby

Вибачте @swooby я не; Я впевнений у чому? А яка мета фрагменту коду, який ви опублікували?
Вільям

2

Ось дещо інший спосіб підійти до проблеми виконання фрагментів у функції зворотного виклику та уникнення проблеми IllegalStateException.

Спочатку створіть призначений для користування інтерфейс.

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

Далі створіть фрагмент для обробки об’єктів MyRunnable. Якщо об’єкт MyRunnable був створений після призупинення діяльності, наприклад, якщо екран повернуто або користувач натискає кнопку додому, він ставиться у чергу для подальшої обробки з новим контекстом. Черга переживає будь-які зміни конфігурації, оскільки для екземпляра setRetain встановлено значення true. Метод runProtected запускається на потоці інтерфейсу користувача, щоб уникнути перегонового стану із прапором 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);
                }
            }
        });
    }
}

Нарешті, фрагмент може бути використаний у головній програмі наступним чином:

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

У своїх проектах я використовую шаблон дизайну спостерігачів для вирішення цього питання. В Android приймачі та наміри трансляції є втіленням цієї моделі.

Що я можу зробити , це створити BroadcastReceiver , який я зареєструвати в / активність фрагмента в onResume і скасування реєстрації в / активність фрагмента в OnPause . У BroadcastReceiver методі «s OnReceive я поставив весь код , який повинен працювати як результат - в BroadcastReceiver - отримання Intent (повідомлення) в який був посланий у ваше застосування в цілому. Щоб збільшити вибірковість того, який тип намірів може отримати ваш фрагмент, ви можете використовувати фільтр намірів, як у наведеному нижче прикладі.

Перевагою такого підходу є те, що Намір (повідомлення) може надсилатися з будь-якого місця у вашій програмі (діалогове вікно, яке відкрилося поверх вашого фрагмента, завдання асинхронізації, інший фрагмент тощо). Параметри можуть передаватися навіть як додаткові додатки.

Ще одна перевага полягає в тому, що цей підхід сумісний з будь-якою версією Android API, оскільки BroadcastReceivers та Intents були введені на рівні 1 API.

Вам не потрібно встановлювати спеціальні дозволи на файл маніфесту вашої програми, за винятком випадків, коли ви плануєте використовувати sendStickyBroadcast (куди потрібно додати 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
Якщо виклик sendBroadcast () в notifyFragment () під час паузи буде вже викликаний, скасувати реєстраціюReceiver (), і, таким чином, жоден приймач не буде навколо, щоб зловити цей намір. Чи не відкине система Android тоді наміри, якщо немає коду, який би негайно впорався з цим?
Стів Б

я думаю, що зелені роботи robobus eventbus липкі повідомлення такі, класно.
j2emanue
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.