Как обрабатывать сообщения обработчика, когда действие / фрагмент приостановлен
Незначительные изменения в моей другой публикации
В основном у меня есть сообщение Handler
в моем Fragment
который получает кучу сообщений, которые могут привести к закрытию или отображению диалогов.
Когда приложение находится в фоновом режиме, я получаю onPause
но потом все равно буду получать мои сообщения, как и следовало ожидать. Однако, поскольку я использую фрагменты, я не могу просто закрыть и показать диалоги, так как это приведет к IllegalStateException
,
Я не могу просто уволить или отменить разрешение потери государства.
Учитывая, что у меня есть Handler
Мне интересно, есть ли рекомендуемый подход к тому, как я должен обрабатывать сообщения, находясь в состоянии паузы.
Одним из возможных решений, которое я рассматриваю, является запись сообщений, поступающих во время паузы, и их воспроизведение на onResume
, Это несколько неудовлетворительно, и я думаю, что в структуре должно быть что-то, чтобы справиться с этим более элегантно.
4 ответа
Хотя операционная система Android, по-видимому, не имеет механизма, который в достаточной степени решает вашу проблему, я полагаю, что этот шаблон обеспечивает относительно простой для реализации обходной путь.
Следующий класс является оберткой android.os.Handler
это буферизует сообщения, когда действие приостанавливается, и воспроизводит их при возобновлении.
Убедитесь, что любой ваш код, который асинхронно изменяет состояние фрагмента (например, commit, dismiss), вызывается только из сообщения в обработчике.
Получите ваш обработчик из 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 показывался бы, если бы кнопка 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
Класс на случай, если любые сообщения должны быть обработаны немедленно, даже когда действие приостановлено. Если сообщение обработано, то должно быть возвращено false, и сообщение будет отброшено.
Немного более простая версия отличного 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
так что вам не нужно управлять этим в подклассе.
Вот немного другой подход к проблеме выполнения фиксации фрагмента в функции обратного вызова и избежания проблемы 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();
...
}
});
}
}
В своих проектах я использую шаблон проектирования наблюдателя, чтобы решить эту проблему. В Android широковещательные приемники и намерения являются реализацией этого паттерна.
Что я делаю, так это создаю BroadcastReceiver, который я регистрирую в onResume фрагмента / активности и отменяю регистрацию в onPause фрагмента / действия. В метод BroadcastReceiver onReceive я поместил весь код, который должен быть запущен в результате - BroadcastReceiver - получения намерения (сообщения), которое было отправлено вашему приложению в целом. Чтобы повысить селективность в отношении того, какие типы намерений может получить ваш фрагмент, вы можете использовать фильтр намерений, как в примере ниже.
Преимущество этого подхода заключается в том, что Intent(сообщение) может быть отправлено из любой точки вашего приложения (диалоговое окно, которое открывается поверх вашего фрагмента, асинхронная задача, другой фрагмент и т. Д.). Параметры могут даже передаваться как преднамеренные дополнения.
Другое преимущество состоит в том, что этот подход совместим с любой версией API Android, поскольку 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);
}
}