Распространенной проблемой является некорректное поведение приложения при повороте девайса. Дело в том что при повороте Activity-host (Activity которое является родителем для фрагмента) уничтожается. В тот момент когда этот процесс происходит FragmentManager отвечает за уничтожение дочернего фрагмента. FragmentManager запускает методы угасающего жизненного цикла фрагмента: onPause(), onStop() и onDestroy().

В случае если в контроллере нашего дочернего фрагмента, к примеру, есть объект Media-Player, то в методе фрагмента Fragment.onDestroy() экземпляр нашего звонко играющего Media-Player-а прервет воспроизведение медиа данных. Первое, что приходит в голову, сохранить состояние объекта Media-Player вызвав Fragment.onSaveInstanceState(Bundle), что сохранит данные, а новое Activity загрузит их. Однако, сохранение состояния объекта MediaPlayer и его последующее восстановление, все равно, прерывает воспроизведение и заставляет акул ненависти сновать в головах пользователей.

Сохранение Fragments


Благо, у Fragment присутствует механизм, при помощи которого экземпляр Media-Player может “пережить” изменение конфигурации. Переопределив метод Fragment.onCreate(...) и задав свойство фрагмента.

@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setRetainInstance(true);
}

Свойство retainInstance фрагмента, по дефолту, является false. Это означает, что при поворотах девайса Fragment не сохраняется, а уничтожается и создается по новому вместе с Activity-host. При вызове setRetainInstance(true) фрагмент не уничтожается вместе с его хостом и передается новому Activity в не измененном виде. При сохранении фрагмента можно рассчитывать на то, что все его поля (включая View) сохранят прежние значения. Мы к ним обращаемся, а они уже есть и все. Используя этот подход можно убедится в том, что при повороте девайса наш объект MediaPlayer не прервет свое воспроизведение и пользователь не будет психовать.

Повороты и сохраненные фрагменты


Теперь стоит взглянуть более подробно на то, как работают сохраненные фрагменты. Fragment руководствуется тем обстоятельством, что представление фрагмента может уничтожаться и создаваться заново без необходимости уничтожать сам Fragment. При изменении конфигурации FragmentManager уничтожает и заново собирает представление фрагментов. Аналогично ведет себя и Activity. Это происходит из соображений что в новой конфигурации могут потребоваться новые ресурсы. На случай, если для нового варианта существуют более подходящие ресурсы, представление строится «с нуля».

FragmentManager проверяет свойство retainInstance каждого фрагмента. Если оно дефолтное (false), FragmentManager уничтожает экземпляр фрагмента. Fragment и его представление будут созданы заново новым экземпляром FragmentManager принадлежащем новой активности.

image

Что же происходит если значение retainInstance равно true. Представление фрагмента уничтожается но сам фрагмент остается. Создастся новый экземпляр Activity, а затем и новый FragmentManager который найдет сохраненный Fragment и воссоздаст его View.

image

Наш сохраненный фрагмент открепляется (detached) от предыдущей Activity и продолжает жить но уже не имея Activity-host.

image

Соответственно, переход в сохраненное состояние происходит при выполнении следующих условий:

  • для фрагмента был вызван метод setRetainInstance(true)
  • Activity-host уничтожается для изменения конфигурации (обычно это поворот девайса)

В этом случае Fragment живет совсем недолго от момента отсоединения от своей первой Activity до передачи его в пользование к немедленно созданной нового Acticvity.

Сохранение фрагментов: действительно так хорошо?


Сохраненные фрагменты: ведь правда, удобно? Да! Действительно удобно. На первый взгляд они решают все проблемы, возникающие с уничтожением Activity и фрагментов при поворотах. При изменении конфигурации устройства для подбора наиболее подходящих ресурсов создается новое представление, а в вашем распоряжении имеется простой способ сохранения данных и объектов.

В таком случае возникает вопрос, почему не сохранять все фрагменты подряд и почему фрагменты не сохраняются по умолчанию? Невольно складывается впечатление, что Android без энтузиазма относится к сохранению Fragment-ов в UI. Мне не ясно, почему это так.
Стоит отметить, что сохраненный Fragment продолжает существовать только при уничтожении Activity — при изменения конфигурации. Если Activity уничтожается из-за того, что ОС потребовалось освободить память, то все сохраненные фрагменты также будут уничтожены.

Пора поговорить и об onSaveInstanceState(Bundle)


Такой подход является более распространенным в борьбе с проблемой потери данных при поворотах. Если ваше преложение с легкостью отрабатывает эту ситуацию то все благодаря работе поведения onSaveInstanceState(...) по дефолту.

Метод onSaveInstanceState(...) проектировался для решения сохранения и восстановления состояния пользовательского интерфейса приложения. Как я думаю, Вы догадались — есть основополагающее различие между этими подходами. Главное отличие между переопределением Fragment.onSaveInstanceState(...) и сохранением Fragment-а — продолжительность существования сохраненных данных.

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

Но в том случае, если данные должны существовать дольше, сохранение фрагмента не поможет. Если Activity уничтожается для освобождения памяти после бездействия юзера, все сохраненные фрагменты уничтожаются так же, как и их не сохраненные родственники.

В заключение


Итак, если в Activity или в Fragment присутствуют данные, которые должны существовать на протяжении долгого времени, их стоит привязать к сроку жизни активности, переопределяя метод onSaveInstanceState(Bundle) для сохранения состояния и его последующего восстановления.

Комментарии (16)


  1. hyperax
    31.03.2016 15:01

    Спасибо за статью!
    Метод onSaveInstanceState имеет множество особенностей применительно к фрагментам и особенно к вложенным фрагментам.
    Для себя выбрал вариант с сохранением состояния внутри аргументов фрагмента.


  1. questman
    31.03.2016 17:15
    +1

    А что думаете насчет отказа от использования фрагментов в пользу одной активити и обычных вьюх?


  1. basnopisets
    31.03.2016 17:41

    setRetainInstance(true) — прекрасный метод. Только стоит упомянуть, что его нельзя использовать в паре с Loader


  1. Alexko
    31.03.2016 18:09

    В статье не упомянуто о том, что setRetainInstance(true) не будет работать для фрагментов, находящихся в back stack (тыц). А это — совсем не маловажный момент.


  1. Dimezis
    01.04.2016 01:15
    +1

    Не надо во фрагменте держать медиаплеер. Вынесите его в сервис.
    Хоть это и не совсем относится к статье.


  1. AlterMax
    01.04.2016 10:11

    А почему на второй картинке в столбце «After device turn» «NEW Fragment»? Полагаю должен быть старый фрагмент…


    1. Philippoid
      01.04.2016 10:12

      Спасибо, поправил.


      1. AlterMax
        01.04.2016 10:24

        я дико извиняюсь, но вы исправили ПЕРВУЮ картинку, а надо было вторую...


        1. Philippoid
          01.04.2016 11:33

          Благодарю за бдительность...


  1. fRoStBiT
    01.04.2016 10:13

    Главное при использовании setRetainInstance(true) — занулять все ссылки на любые View в onDestroyView(). Иначе старая Activity утечёт на некоторое время.
    Ну и не надо забывать, что сохранять состояние в Bundle необходимо в любом случае. Иначе краткое переключение на какое-нибудь тяжёлое приложение (например, браузер) может запросто уничтожить состояние UI, что очень неприятно для пользователя.


  1. OlegKrikun
    01.04.2016 10:13

    Насколько я помню setRetainInstance(true) рекомендуется использовать в крайних случаях (или для фрагментов которые не имею UI). А вот проблема с прерыванием воспроизведения при перевороте решается с помощью Service.


  1. Whiskas333
    01.04.2016 10:13

    Спасибо за статью!


    1. Philippoid
      01.04.2016 11:44

      Пожалуйста, в перспективе еще несколько статей раскрывающих с той или иной стороны реализацию и использование фрагментов.


  1. lavelas
    01.04.2016 13:55

    Использую RetainInstance фрагменты только для сохранения объектов, которые должны пережить разворот экрана, например момент авторизации пользователя, заливка файла и прочее.
    Сохраняемый объект при этом должен реализовывать паттерн Observer and Observable.
    Кому интересно, пример:

    ModelStorage.java
    public class ModelStorage extends Fragment {
        public static final String TAG = ModelStorage.class.getCanonicalName();
        private CallbackModel model = new CallbackModel();
    
        @Override
        public void onCreate(Bundle savedInstanceState) {
            super.onCreate(savedInstanceState);
            setRetainInstance(true);
        }
    
        public CallbackModel getCallbackModel(){
            return model;
        }


  1. skssxf
    02.04.2016 19:03

    Иногда для сохранения объекта используют экземпляр приложения, т.е. выносят на уровень выше и фрагмента, и активности.


    1. lavelas
      03.04.2016 15:14

      Это неудобно. Экземпляр приложения один, а таких фрагментом может быть много.