Как обрабатывать сообщения обработчика, когда действие / фрагмент приостановлено

98

Небольшое изменение в другом моем сообщении

В принципе у меня есть сообщение Handlerв моем Fragmentкоторая получает кучу сообщений , которые могут привести к диалогам увольняют или показано на рисунке.

Когда приложение переводится в фоновый режим, я получаю сообщение, onPauseно все равно получаю сообщения, как и следовало ожидать. Однако, поскольку я использую фрагменты, я не могу просто закрыть и показать диалоги, так как это приведет к расширению IllegalStateException.

Я не могу просто отклонить или отменить разрешение на потерю состояния.

Учитывая, что у меня есть, Handlerмне интересно, есть ли рекомендуемый подход к тому, как я должен обрабатывать сообщения в состоянии паузы.

Одно из возможных решений, которое я рассматриваю, - это записывать сообщения, приходящие во время паузы, и воспроизводить их на onResume. Это несколько неудовлетворительно, и я думаю, что во фреймворке должно быть что-то, чтобы справиться с этим более элегантно.

PJL
источник
1
вы можете удалить все сообщения в обработчике в методе фрагмента onPause (), но есть проблема с восстановлением сообщений, что, как мне кажется, невозможно.
Yashwanth Kumar

Ответы:

166

Хотя операционная система 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класса.

При нажатии кнопки обработчику отправляется задержанное сообщение.

Когда обработчик получает сообщение (в потоке пользовательского интерфейса), он отображает файл DialogFragment.

Если PausedHandlerкласс не использовался, было бы показано исключение IllegalStateException, если после нажатия кнопки проверки для запуска диалогового окна была нажата кнопка «Домой».

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класс метод на тот случай, если какие-либо сообщения должны обрабатываться немедленно, даже если действие приостановлено. Если сообщение обрабатывается, должно быть возвращено false, и сообщение будет отклонено.

оттяжка макгроу
источник
26
Хорошее решение, работает приятно. Не могу не думать, что фреймворк должен справиться с этим.
PJL
1
как передать обратный вызов DialogFragment?
Malachiasz
Я не уверен, что понимаю вопрос Малахиаса, не могли бы вы уточнить.
quickdraw mcgraw
Это очень элегантное решение! Если я не ошибаюсь, поскольку resumeметод sendMessage(msg)технически использует, могут быть другие потоки, помещающие сообщение в очередь прямо перед (или между итерациями цикла), что означает, что сохраненные сообщения могут чередоваться с поступающими новыми сообщениями. Не уверен, что это имеет большое значение. Может быть, использование sendMessageAtFrontOfQueue(и, конечно, обратная итерация) решит эту проблему?
ян
4
Я думаю, что этот подход может не всегда работать - если действие уничтожено ОС, список сообщений, ожидающих обработки, будет пустым после возобновления.
GaRRaPeTa 08
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);

}

Он предполагает, что вы всегда хотите хранить автономные сообщения для воспроизведения. И предоставляет Activity в качестве входных данных, #processMessagesпоэтому вам не нужно управлять им в подклассе.

Уильям
источник
Почему ваши resume()и pause(), и handleMessage synchronized?
Максим Дмитриев
5
Потому что вы не хотите, чтобы во время #handleMessage вызывалась #pause, и вы вдруг обнаружите, что активность равна нулю, когда вы используете ее в #handleMessage. Это синхронизация общего состояния.
Уильям
@William Не могли бы вы объяснить мне подробнее, зачем вам синхронизация в классе PauseHandler? Кажется, что этот класс работает только в одном потоке, потоке пользовательского интерфейса. Я предполагаю, что #pause не может быть вызван во время #handleMessage, потому что оба они работают в потоке пользовательского интерфейса.
Samik
@ Уильям ты уверен? HandlerThread handlerThread = новый HandlerThread ("mHandlerNonMainThread"); handlerThread.start (); Looper looperNonMainThread = handlerThread.getLooper (); Обработчик handlerNonMainThread = новый обработчик (looperNonMainThread, new Callback () {публичный логический 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();
                ...
            }
         });
    }
}
Rua109
источник
0

В своих проектах я использую шаблон проектирования наблюдателя, чтобы решить эту проблему. В Android широковещательные приемники и намерения являются реализацией этого шаблона.

Что я могу сделать , это создать BroadcastReceiver , который я зарегистрировать в / активность фрагмента в onResume и отмены регистрации в / активность фрагмента в OnPause . В BroadcastReceiver методе «s OnReceive я поставил весь код , который должен работать как результат - в BroadcastReceiver - получение Intent (сообщение) в который был послан в ваше приложение в целом. Чтобы повысить избирательность в отношении того, какие типы намерений может принимать ваш фрагмент, вы можете использовать фильтр намерений, как в примере ниже.

Преимущество этого подхода заключается в том, что намерение (сообщение) может быть отправлено отовсюду в вашем приложении (диалоговое окно, открывающееся поверх вашего фрагмента, асинхронная задача, другой фрагмент и т. Д.). Параметры могут даже передаваться как дополнительные функции.

Еще одно преимущество заключается в том, что этот подход совместим с любой версией Android API, поскольку BroadcastReceivers и Intents были представлены на уровне API 1.

Вам не требуется устанавливать какие-либо особые разрешения для файла манифеста вашего приложения, за исключением случаев, когда вы планируете использовать 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 () вызывается во время состояния Pause, unregisterReceiver () уже будет вызван и, следовательно, не будет получателя, который мог бы поймать это намерение. Разве система Android не откажется от этого намерения, если нет кода для его немедленной обработки?
Steve B
Я думаю, что липкие сообщения в автобусе событий зеленого робота такие, круто.
j2emanue