В случае если в контроллере нашего дочернего фрагмента, к примеру, есть объект 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 принадлежащем новой активности.
Что же происходит если значение retainInstance равно true. Представление фрагмента уничтожается но сам фрагмент остается. Создастся новый экземпляр Activity, а затем и новый FragmentManager который найдет сохраненный Fragment и воссоздаст его View.
Наш сохраненный фрагмент открепляется (detached) от предыдущей Activity и продолжает жить но уже не имея Activity-host.
Соответственно, переход в сохраненное состояние происходит при выполнении следующих условий:
- для фрагмента был вызван метод 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)
questman
31.03.2016 17:15+1А что думаете насчет отказа от использования фрагментов в пользу одной активити и обычных вьюх?
basnopisets
31.03.2016 17:41setRetainInstance(true)
— прекрасный метод. Только стоит упомянуть, что его нельзя использовать в паре с Loader
Dimezis
01.04.2016 01:15+1Не надо во фрагменте держать медиаплеер. Вынесите его в сервис.
Хоть это и не совсем относится к статье.
AlterMax
01.04.2016 10:11А почему на второй картинке в столбце «After device turn» «NEW Fragment»? Полагаю должен быть старый фрагмент…
Philippoid
01.04.2016 10:12Спасибо, поправил.
fRoStBiT
01.04.2016 10:13Главное при использовании setRetainInstance(true) — занулять все ссылки на любые View в onDestroyView(). Иначе старая Activity утечёт на некоторое время.
Ну и не надо забывать, что сохранять состояние в Bundle необходимо в любом случае. Иначе краткое переключение на какое-нибудь тяжёлое приложение (например, браузер) может запросто уничтожить состояние UI, что очень неприятно для пользователя.
OlegKrikun
01.04.2016 10:13Насколько я помню setRetainInstance(true) рекомендуется использовать в крайних случаях (или для фрагментов которые не имею UI). А вот проблема с прерыванием воспроизведения при перевороте решается с помощью Service.
Whiskas333
01.04.2016 10:13Спасибо за статью!
Philippoid
01.04.2016 11:44Пожалуйста, в перспективе еще несколько статей раскрывающих с той или иной стороны реализацию и использование фрагментов.
lavelas
01.04.2016 13:55Использую RetainInstance фрагменты только для сохранения объектов, которые должны пережить разворот экрана, например момент авторизации пользователя, заливка файла и прочее.
Сохраняемый объект при этом должен реализовывать паттерн Observer and Observable.
Кому интересно, пример:
ModelStorage.javapublic 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; }
hyperax
Спасибо за статью!
Метод onSaveInstanceState имеет множество особенностей применительно к фрагментам и особенно к вложенным фрагментам.
Для себя выбрал вариант с сохранением состояния внутри аргументов фрагмента.