- Fragment (Фрагменты). Часть пятая
- Сохранение данных
- Раз и навсегда, как правильно сохранить состояние экземпляра фрагментов в заднем стеке?
- Сохранение состояния фрагментов (Fragment)
- Сохранение Fragments
- Повороты и сохраненные фрагменты
- Сохранение фрагментов: действительно так хорошо?
- Пора поговорить и об onSaveInstanceState(Bundle)
- Cохранение состояний в android приложениях
Fragment (Фрагменты). Часть пятая
Сохранение данных
Теперь рассмотрим важный вопрос, как сохранять данные при изменении конфигурации и других операциях.
Для примера возьмём старый урок по подсчёту ворон, но на этот раз будем считать котов. Вороны вечно порхают с ветки на ветку, их сложно считать. Коты — совсем другое дело, они лежат себе на одном месте и спят. Считать их одно удовольствие.
Кажется, я поторопился со своим утверждением.
Новый проект создавать не будем, а модифицируем старый. Заодно закрепим материал.
Создадим в проекте два новых фрагмента: WithButtonFragment и WithTextViewFragment. Из названий понятно, что в первом фрагменте будет кнопка, а во втором — TextView, в котором будет отражаться информация о количестве котов.
Повторяем прошлые шаги. Создаём новый класс, наследуемся от Fragment.
Создадим две разметки для них. Для удобства будем использовать фон, чтобы различать фрагменты на экране.
Подключаем разметки к соответствующим фрагментам в методе onCreateView().
Для второго фрагмента напишите код самостоятельно.
Подключаем фрагменты в разметке основной активности activity_main.xml:
Не копируйте код с сайта, проделывайте операции самостоятельно через редактор в режиме Design и Text, как это объяснялось в предыдущих примерах.
Подключим кнопку в первом фрагменте.
Во втором фрагменте подключим компонент TextView и добавим метод для вывода текста.
Если мы вызовем метод changeText() с каким-нибудь текстом, то текст отобразится в TextView. Наша задача — научить фрагменты обмениваться данными через активность.
Если сейчас запустить пример, то фрагменты будут работать независимо друг от друга. Счётчик будет увеличиваться, но его значения пока не попадают во второй фрагмент и мы не можем увидеть число котов.
Напоминаю, мы не связываем два фрагмента между собой напрямую. Создаём интерфейс для этих целей.
В предыдущем примере мы создавали интерфейс внутри первого фрагмента. Для разнообразия изменим подход. Создадим новый класс через New | Java Class и в диалоговом окне для поля Kind выберем Interface. Также зададим ему имя Communicator.
Интерфейс будет состоять из одного метода.
Активность должна реализовать интерфейс.
Фрагмент с кнопкой может использовать объект интерфейса для отправки данных. Инициализируем его в новом методе onActivityCreated(), который ранее не использовали. Метод сработает, когда активность будет создана и готова к работе.
При щелчках на кнопках мы вызываем метод count(), которому передаём нужную информацию.
А сам метод в активности будет выглядеть следующим образом:
Фрагмент передаёт через метод count() данные data, а активность их принимает и передаёт их в метод второго фрагмента changeText().
Подготовительные работы завершены и можно запустить пример для проверки.
Если вы создавали пример на основе предыдущих уроков, то не поворачивайте экран. Сначала удалите (или переименуйте) файлы для альбомной ориентации, чтобы не получить ошибку и крах приложения.
Теперь переходим непосредственно к теме нашего урока. Повернув экран, мы обнаружим, что данные из текстового блока пропали. Наши подсчёты — коту под хвост!
Мы можем в новой ориентации начать новый подсчёт, но повернув устройство обратно в портретный режим, снова потеряем данные.
Мы знаем, что при поворотах активность создаётся заново. Поэтому все данные сбрасываются. Чтобы сохранить данные, у фрагментов есть соответствующие методы, схожие с подобными методами у активностей. Задействуем их.
Метод onSaveInstanceState() поможет нам. Добавим метод в первый фрагмент с кнопкой.
Параметр метода outState является объектом класса Bundle и позволяет хранить различные типы в формате «ключ-значение».
У фрагментов также есть метод onCreate(Bundle savedInstanceState), где используется объект этого же класса Bundle только под другим именем savedInstanceState. Несмотря на разные имена, речь идёт об одном и том же объекте. И сохраняя данные в методе onSaveInstanceState(), мы можем их получить в методе onCreate():
При повороте фрагмент сохранит значение счётчика, перезапустится и восстановит значение счётчика. Если запустить пример, то увидим следующее. Щёлкнем несколько раз по кнопке и повернём экран. Данные сбросятся и мы снова увидим пустой текст. Но стоит нам нажать на кнопку, то увидим, что отсчёт пошёл не сначала, а продолжил со своего последнего значения. Мы видим, что первый фрагмент запоминает свои данные. А второй фрагмент пока тупит. Поможем ему.
Добавим новую переменную mData, которая будет хранить текст сообщения. У метода onCreateView() также есть параметр savedInstanceState класса Bundle, позволяющий извлечь сохранённые данные.
А текст мы сохраним в методе onSaveInstanceState(). В методе changeText() добавим строку кода, чтобы текст брался из новой переменной. Теперь при первом запуске всё работает как прежде. При повороте текст из mData сохраняется в методе putString() и восстанавливается через getString().
После этих изменений программа больше не теряет своих данных и можно спокойно считать котов. Я стал смотреть на гифку и считать. Через 8 минут 16 секунд последний кот выпрыгнул в окно и в итоге получилось 154578 котов. Перепроверьте.
Источник
Раз и навсегда, как правильно сохранить состояние экземпляра фрагментов в заднем стеке?
Я обнаружил много случаев аналогичного вопроса в SO, но, к сожалению, ни один ответ не отвечает моим требованиям.
У меня есть разные макеты для портретной и альбомной ориентации, и я использую задний стек, который не позволяет мне использовать setRetainState() и хитрости при использовании процедур изменения конфигурации.
Я показываю определенную информацию пользователю в TextViews, которая не сохраняется в обработчике по умолчанию. При написании моего приложения исключительно с использованием Activity, хорошо работало следующее:
С Fragment s это работает только в очень специфических ситуациях. В частности, ужасно ломается замена фрагмента, помещение его в задний стек, а затем вращение экрана, пока отображается новый фрагмент. Из того, что я понял, старый фрагмент не получает вызов onSaveInstanceState() при замене, но остается каким-то образом связанным с Activity , и этот метод вызывается позже, когда его View больше не существует, поэтому поиск любого из моих TextView s приводит к NullPointerException ,.
Кроме того, я обнаружил, что сохранение ссылки на мое TextViews не является хорошей идеей с Fragment s, даже если это было нормально с Activity . В этом случае onSaveInstanceState() фактически сохраняет состояние, но проблема появляется снова, если я поворачиваю экран дважды, когда фрагмент скрыт, так как его onCreateView() не вызывается в новом экземпляре.
Я думал о сохранении состояния в onDestroyView() в некоторый элемент-член класса типа Bundle (на самом деле это больше данных, а не просто одно TextView ) и сохранение that в onSaveInstanceState() , но есть и другие недостатки. Прежде всего, если фрагмент есть, показанный в данный момент, порядок вызова двух функций меняется на обратный, поэтому мне нужно учитывать две разные ситуации. Должно быть более чистое и правильное решение!
Источник
Сохранение состояния фрагментов (Fragment)
Распространенной проблемой является некорректное поведение приложения при повороте девайса. Дело в том что при повороте 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(. ) и задав свойство фрагмента.
Свойство 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 уничтожается для освобождения памяти после бездействия юзера, все сохраненные фрагменты уничтожаются так же, как и их не сохраненные родственники.
Источник
Cохранение состояний в android приложениях
Сегодня я хотел поделиться с вами еще одним подходом сохранения состояния при разработке android приложений. Не для кого не секрет, что наше приложение в фоне может быть убито в любой момент и эта проблема становится все актуальнее с вводом агрессивного энергосбережения – привет Oreo. Также никто не отменял смену конфигурации на телефоне: ориентация, смена языка и т.д. И чтобы открыть приложение из бэкграунда и отобразить интерфейс в последнем состоянии нам нужно позаботиться о его сохранении. Ох уж этот onSaveInstanceState.
Сколько боли он нам принес.
Далее я буду приводить примеры, ипользуя Clean Achitecture и Dagger2, так что будьте готовы к этому:)
Вопрос сохранения состояния в зависимости от задач можно решить несколькими способами:
- Сохранять первичные данные в onSaveInstanceState хоста (Activity, Fragment) — такие как айдишник страницы, пользователя, да что угодно. То, что нам требуется для первичного получения данных и отображения страницы.
- Сохранять полученные данные в интеракторе в репозитории (SharedPreference, Database.
- Использовать ретеин фрагменты для сохранения и восстановления данных при пересоздании активити.
Но что делать, если нам нужно восстановить состояние ui, а также текущую реакцию интерфейса на действие пользователя? Для большей простоты рассмотрим решение этой задачи на реальном примере. У нас есть страница логина — пользователь вводит свои данные, нажимает на кнопку и тут к нему поступает входящий звонок. Наше приложение уходит в бэкграунд. Его убивает система. Звучит страшновато, не правда ли?)
Пользователь возвращается к приложению и что он должен увидеть? Как минимум, продолжение операции логина и показ прогресса. Если приложение успело пройти логин до вызова метода onDestroy хоста, то тогда пользователь увидит навигацию на стартовый экран приложения. Данное поведение можно с легкостью решить, используя паттерн состояния (State machine). Очень хороший доклад от яндекс. В этой же статье постараюсь поделиться пережеванными мыслями по этому докладу.
Теперь немного кода:
В нашем случае state owner будет презентер.
Рассматривая страницу логина можно выделить три уникальных состояния:
LoginInitState, LoginProgressingState, LoginCompleteState.
Итак, рассмотрим теперь, что происходит в этих состояниях.
LoginInitState у нас происходит валидация полей и в случае успешной валидации кнопка login становится активной.
В LoginProgressingState делается запрос логина, сохраняется токен, делаются дополнительные запросы для старта главной активити приложения.
В LoginCompleteState осуществляется навигация на главный экран приложения.
Условно переход между состояниями можно отобразить на следующей диаграмме:
Выход из состояния LoginProgressingState происходит в случае успешной операции логина в состояние LoginCompleteState, а в случае сбоя в LoginInitState. Таким образом, когда у нас вьюха детачится, мы имеем вполне детерменированное состояние презентера. Это состояние мы должны сохранить, используя стандартный механизм андроида onSaveInstanceState. Для того, чтобы мы могли это сделать, все состояния логина должны имплементировать интерфейс Parcelable. Поэтому расширяем наш базовый интерфейс BaseState.
Далее у нас встает вопрос, как пробросить это состояние из презентера в наш хост? Самый простой способ — из хоста попросить данные у презентера, но с точки зрения архитектуры это выглядит не очень. И поэтому нам на помощь приходят retain фрагменты. Мы можем создать интерфейс для кэша и имплементировать его в таком фрагменте:
Далее мы инжектим кэш фрагмент в конструктор интерактора, как Cache. Добавляем методы в интеректоре для получения и сохранения состояния в кэше. Теперь, при каждом изменении состояния презентера, мы можем сохранить состояние в интеракторе, а интерактор сохраняет в свою очередь в кэше. Все становится весьма логично. При первичной загрузке хоста, презентер получает состояние у интерактора, который в свою очередь получает данные из кэша. Так выглядит метод изменения состояния в презентере:
Хочется отметить такой момент — сохранение данных через кэш можно производить для любых данных, не только для состояния. Возможно, вам придется сделать свой уникальный кэш фрагмент для хранения текущих данных. В данной статье рассказан общий подход. Также хочется отметить, что рассматриваемая ситуация очень утрированная. В жизни приходится решать задачи намного сложнее. К примеру, у нас в приложении были совмещены три страницы: логин, регистрация, восстановления пароля. При этом диаграмма состояний выглядела следующим образом:
В итоге, используя паттерн состояний и подход, описанный в статье, нам удалось сделать код более читаемым и поддерживаемым. И что не мало важно — восстанавливать текущее состояние приложения.
Источник