Транзакции фрагментов и потеря состояния активности
Для тех, кто не сталкивался с этой проблемой, поясню на примере — в конце длительной фоновой операции вы показываете диалог (да Google не рекомендует так делать, но заказчик требует). Если до показа диалога вы свернете приложение нажав клавишу Home, то во время показа диалога произойдет исключение IllegalStateException. То же самое произойдет в случае показа диалога ожидания и скрытия его по завершению фоновой активности — вызов метода dismiss () после сохранения состояния вызовет исключение.
Лучшая статья на эту тему, которую я нашел погуглив проблему это Fragment Transactions & Activity State Loss. Статья объясняет проблему, но дает только общие советы, сама проблема остается нерешенной. Возможно кому-то из хабражителей будет интересно сделать перевод статьи, а пока расскажу вкратце ее смысл. Система Android обладает возможностью завершить любую активность вашего приложения и ее фрагменты при нехватке памяти. Чтобы скрыть от пользователя этот прискорбный факт, Android сохраняет состояние активности и восстанавливает его при необходимости, так что пользователь даже не замечает какие катаклизмы происходили на уровне кода. Когда вы пытаетесь отобразить диалог после сохранения состояния, по сути вы нарушаете сохраненное состояние и такая активность не может быть восстановлена. Android решает это простейшим для себя способом — выкидывает исключение и не позволяет закомитить транзакцию фрагментов. А ваше приложение просто крашится.
Способов бороться с этой проблемой масса, все они сводятся к тому чтобы отложить транзакцию на момент после восстановления, а именно в функциях Activity.onPostResume или Fragment.onResume. Первое что может прийти в голову — вместо показа диалога поставить флажок, проверить его в onResume и отобразить диалог там. В случае onActivityResult это даже сработает — потому что эта функция всегда вызывается до восстановления состояния активности. Но в случае фоновой обработки вы даже не знаете в каком состоянии будет активность, а простого метода проверки состояния активности просто нет. Рекомендуемый Google способ это использовать Loader для фоновой обработки. Однако не всегда это удобно. Например, их же библиотека Volley, не использует этот шаблон и простого способа подключить его нет.
Не буду томить вас другими неудачными попытками обойти проблему. Думаю у многих из вас есть какие-то варианты, я же поделюсь своим решением.
Напишем тестовое приложение:
public class MainActivity extends ActionBarActivity {
@Override protected void onCreate (Bundle savedInstanceState) { super.onCreate (savedInstanceState); setContentView (R.layout.activity_main);
startAsyncTask (); }
private void startAsyncTask () {
new AsyncTask
@Override protected Void doInBackground (Void… params) { try { Thread.sleep (3000); } catch (InterruptedException e) { e.printStackTrace (); } runOnUiThread (new Runnable () { @Override public void run () { showMyDialog (); } }); return null; } }.execute (); }
private void showMyDialog () { new TestDialog ().show (getSupportFragmentManager (), «dialog»); }
public class TestDialog extends DialogFragment { public TestDialog (){}
@Override public Dialog onCreateDialog (Bundle savedInstanceState){ AlertDialog.Builder builder = new AlertDialog.Builder (MainActivity.this); builder.setMessage («Hello World»); builder.setPositiveButton («OK», null); return builder.create (); } } } Если вы запустите его, то через три секунды оно отобразит диалог. Но если до истечения трех секунд вы нажмете клавишу Home, то при показе диалога приложение аварийно завершится.
Теперь добавим следующий класс, он будет отслеживать состояние активности для нас:
public class StateHandler {
/**
* Очередь объектов Runnable
*/
private final List
/** * Флаг, показывающий состояние активности */ private Activity activity;
/** * Обработчик восстановления */ public final synchronized void resume (Activity activity) { this.activity = activity;
while (queueBuffer.size () > 0) { final Runnable runnable = queueBuffer.get (0); queueBuffer.remove (0); runnable.run (); } }
/** * Обработчик паузы */ public final synchronized void pause () { activity = null; }
/** * Сохраняем Runnable если мы в состоянии паузы, либо сразу выполняем его код * * @param runnable объект Runnable, который будет запускаться. */ public final synchronized void run (Runnable runnable) { if (activity == null) { queueBuffer.add (runnable); } else { runnable.run (); } } } Чтобы не повторять рутинные действия по инициализации и поддержке класса обработки состояния вынесем их базовую активность:
public class BaseActivity extends ActionBarActivity { protected StateHandler stateHandler = new StateHandler ();
@Override protected void onPause () { super.onPause (); stateHandler.pause (); }
@Override protected void onPostResume () { super.onPostResume (); stateHandler.resume (this); } } Все что останется сделать это обернуть транзакции с помощью нашего нового обработчика состояний:
stateHandler.run (new Runnable () { @Override public void run () { new TestDialog ().show (getSupportFragmentManager (), «dialog»); } }); В случае, если состояние позволяет, код внутри Runnable будет незамедлительно выполнен. В противном случае код поместится в очередь и будет выполнен после восстановления активности.
Точно так же, вы можете использовать этот класс внутри фрагментов, только вместо метода onPostResume вызовите код обработчика из метода onResume базового фрагмента.
Исходный код примера можно найти на github.