Основы Android Kotlin 04.2: Сложные ситуации жизненного цикла

Эта практическая работа входит в курс «Основы Android Kotlin». Вы получите максимальную пользу от этого курса, если будете выполнять практические работы последовательно. Все практические работы курса перечислены на целевой странице практической работы «Основы Android Kotlin» .

Введение

В предыдущей лабораторной работе вы узнали о жизненных циклах Activity и Fragment , а также о методах, вызываемых при изменении состояния жизненного цикла в Activity и Fragment. В этой лабораторной работе вы более подробно изучите жизненный цикл Activity. Вы также познакомитесь с библиотекой жизненного цикла Android Jetpack, которая поможет вам управлять событиями жизненного цикла с помощью более структурированного и удобного в поддержке кода.

Что вам уже следует знать

  • Что такое активность и как создать ее в своем приложении.
  • Основы жизненных циклов Activity и Fragment , а также обратные вызовы, которые вызываются при переходе активности между состояниями.
  • Как переопределить методы обратного вызова жизненного цикла onCreate() и onStop() для выполнения операций в разные моменты жизненного цикла активности или фрагмента.

Чему вы научитесь

  • Как настраивать, запускать и останавливать части вашего приложения в обратных вызовах жизненного цикла.
  • Как использовать библиотеку жизненного цикла Android для создания наблюдателя жизненного цикла и упрощения управления жизненным циклом активности и фрагмента.
  • Как завершение процессов Android влияет на данные в вашем приложении и как автоматически сохранять и восстанавливать эти данные, когда Android закрывает ваше приложение.
  • Как поворот устройства и другие изменения конфигурации приводят к изменениям состояний жизненного цикла и влияют на состояние вашего приложения.

Что ты будешь делать?

  • Измените приложение DessertClicker, включив в него функцию таймера, а также запускайте и останавливайте этот таймер в различные моменты жизненного цикла действия.
  • Измените приложение так, чтобы оно использовало библиотеку жизненного цикла Android, и преобразуйте класс DessertTimer в наблюдатель жизненного цикла.
  • Настройте и используйте Android Debug Bridge ( adb ) для имитации завершения процесса вашего приложения и обратных вызовов жизненного цикла, которые происходят при этом.
  • Реализуйте метод onSaveInstanceState() для сохранения данных приложения, которые могут быть потеряны при его неожиданном закрытии. Добавьте код для восстановления этих данных при следующем запуске приложения.

В этой практической работе вы расширите приложение DessertClicker из предыдущей. Вы добавите фоновый таймер, а затем преобразуете приложение для использования библиотеки жизненного цикла Android .

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

Шаг 1: Настройка DessertTimer

  1. Откройте приложение DessertClicker из последней лабораторной работы. (Если у вас нет приложения, вы можете скачать DessertClickerLogs здесь .)
  2. В окне проекта разверните j ava > com.example.android.dessertclicker и откройте DessertTimer.kt . Обратите внимание, что сейчас весь код закомментирован, поэтому он не запускается как часть приложения.
  3. Выделите весь код в окне редактора. Выберите «Код» > «Комментировать строкой» или нажмите Control+/ ( Command+/ на Mac). Эта команда раскомментирует весь код в файле. (В Android Studio могут отображаться неисправленные ошибки ссылок до тех пор, пока вы не пересоберете приложение.)
  4. Обратите внимание, что класс DessertTimer включает startTimer() и stopTimer() , которые запускают и останавливают таймер. При запуске startTimer() таймер каждую секунду выводит сообщение в журнал с указанием общего количества секунд, прошедших с момента отсчёта времени. Метод stopTimer() , в свою очередь, останавливает таймер и операторы журнала.
  1. Откройте MainActivity.kt . В верхней части класса, сразу под переменной dessertsSold , добавьте переменную для таймера:
private lateinit var dessertTimer : DessertTimer;
  1. Прокрутите вниз до onCreate() и создайте новый объект DessertTimer сразу после вызова setOnClickListener() :
dessertTimer = DessertTimer()


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

Шаг 2: Запуск и остановка таймера

Метод onStart() вызывается непосредственно перед тем, как активность становится видимой. Метод onStop() вызывается после того, как активность становится невидимой. Эти обратные вызовы кажутся хорошими кандидатами для запуска и остановки таймера.

  1. В классе MainActivity запустите таймер в обратном вызове onStart() :
override fun onStart() {
   super.onStart()
   dessertTimer.startTimer()

   Timber.i("onStart called")
}
  1. Остановите таймер в onStop() :
override fun onStop() {
   super.onStop()
   dessertTimer.stopTimer()

   Timber.i("onStop Called")
}
  1. Скомпилируйте и запустите приложение. В Android Studio откройте панель Logcat . В поле поиска Logcat введите dessertclicker , что позволит отфильтровать данные по классам MainActivity и DessertTimer . Обратите внимание, что после запуска приложения таймер также начинает работать.
  2. Нажмите кнопку «Назад» и обратите внимание, что таймер снова остановился. Таймер остановился, поскольку и активность, и управляемый ею таймер были уничтожены.
  3. Чтобы вернуться в приложение, используйте экран «Недавние». Обратите внимание, что в Logcat таймер перезапускается с 0.
  4. Нажмите кнопку «Поделиться» . Обратите внимание, что в Logcat таймер всё ещё работает.

  5. Нажмите кнопку «Домой» . Обратите внимание, что в Logcat таймер остановился.
  6. Чтобы вернуться в приложение, используйте экран «Недавние». Обратите внимание, что в Logcat таймер запускается с того места, где он остановился.
  7. В методе onStop() метода MainActivity закомментируйте вызов stopTimer() . Закомментирование stopTimer() демонстрирует случай, когда вы запускаете операцию в onStart() , но забываете остановить её в onStop() .
  8. Скомпилируйте и запустите приложение, а затем нажмите кнопку «Домой» после запуска таймера. Несмотря на то, что приложение работает в фоновом режиме, таймер продолжает работать и постоянно использует системные ресурсы. Постоянная работа таймера приводит к утечке памяти в вашем приложении и, вероятно, не соответствует вашим ожиданиям.

    Общая схема такова: когда вы устанавливаете или запускаете что-либо в обратном вызове, вы останавливаете или удаляете это в соответствующем обратном вызове. Таким образом, вы избегаете необходимости запускать что-либо, когда это больше не нужно.
  1. Раскомментируйте строку в onStop() , где останавливается таймер.
  2. Вырежьте и вставьте вызов startTimer() из onStart() в onCreate() . Это изменение демонстрирует случай, когда вы инициализируете и запускаете ресурс в onCreate() , а не используете onCreate() для инициализации и onStart() для запуска.
  3. Скомпилируйте и запустите приложение. Обратите внимание, что таймер, как и ожидалось, начал отсчёт.
  4. Нажмите «Домой», чтобы остановить приложение. Таймер, как и ожидалось, остановится.
  5. Используйте экран недавних приложений для возврата в приложение. Обратите внимание, что в этом случае таймер не запускается снова, поскольку onCreate() вызывается только при запуске приложения, а не при его возврате на передний план.

Основные моменты, которые следует помнить:

  • При настройке ресурса в обратном вызове жизненного цикла также удалите ресурс.
  • Выполняйте настройку и демонтаж соответствующими методами.
  • Если вы что-то настроили в onStart() , остановите или отмените это снова в onStop() .

В приложении DessertClicker довольно легко заметить, что если вы запустили таймер в onStart() , то вам нужно остановить его в onStop() . Таймер всего один, поэтому его остановку несложно запомнить.

В более сложном приложении для Android вы можете настроить множество параметров в onStart() или onCreate() , а затем отключить их в onStop() или onDestroy() . Например, у вас могут быть анимации, музыка, датчики или таймеры, которые нужно настроить и отключить, а также запустить и остановить. Если вы забудете что-то, это приведёт к ошибкам и проблемам.

Библиотека жизненного цикла , входящая в состав Android Jetpack , упрощает эту задачу. Она особенно полезна, когда необходимо отслеживать множество изменяемых компонентов, некоторые из которых находятся на разных этапах жизненного цикла. Библиотека меняет принцип работы жизненных циклов: обычно активность или фрагмент сообщают компоненту (например, DessertTimer ), что делать при возникновении обратного вызова жизненного цикла. Но при использовании библиотеки жизненного цикла компонент сам отслеживает изменения жизненного цикла, а затем выполняет необходимые действия при их возникновении.

Библиотека жизненного цикла состоит из трех основных частей:

  • Владельцы жизненного цикла — это компоненты, имеющие (и, следовательно, «владеющие») жизненным циклом. Activity и Fragment являются владельцами жизненного цикла. Владельцы жизненного цикла реализуют интерфейс LifecycleOwner .
  • Класс Lifecycle , который хранит фактическое состояние владельца жизненного цикла и запускает события при возникновении изменений в жизненном цикле.
  • Наблюдатели жизненного цикла, которые отслеживают состояние жизненного цикла и выполняют задачи при его изменении. Наблюдатели жизненного цикла реализуют интерфейс LifecycleObserver .

В этом задании вы преобразуете приложение DessertClicker для использования библиотеки жизненного цикла Android и узнаете, как эта библиотека упрощает управление работой с жизненными циклами Android и фрагментов.

Шаг 1: Превратите DessertTimer в LifecycleObserver

Ключевой частью библиотеки жизненного цикла является концепция наблюдения за жизненным циклом . Наблюдение позволяет классам (например, DessertTimer ) получать информацию о жизненном цикле активности или фрагмента, а также запускать и останавливать себя в ответ на изменения этих состояний. С помощью наблюдателя за жизненным циклом можно снять ответственность за запуск и остановку объектов с методов активности и фрагмента.

  1. Откройте класс DesertTimer.kt .
  2. Измените сигнатуру класса DessertTimer следующим образом:
class DessertTimer(lifecycle: Lifecycle) : LifecycleObserver {

Это новое определение класса выполняет две функции:

  • Конструктор принимает объект Lifecycle , представляющий собой жизненный цикл, за которым наблюдает таймер.
  • Определение класса реализует интерфейс LifecycleObserver .
  1. Ниже переменной runnable добавьте блок init к определению класса. В блоке init используйте метод addObserver() для подключения объекта жизненного цикла, переданного владельцем (активностью), к этому классу (наблюдателю).
 init {
   lifecycle.addObserver(this)
}
  1. Добавьте к методу startTimer() @OnLifecycleEvent annotation и используйте событие жизненного цикла ON_START . Все события жизненного цикла, которые может отслеживать ваш наблюдатель, находятся в классе Lifecycle.Event .
@OnLifecycleEvent(Lifecycle.Event.ON_START)
fun startTimer() {
  1. Сделайте то же самое для stopTimer() , используя событие ON_STOP :
@OnLifecycleEvent(Lifecycle.Event.ON_STOP)
fun stopTimer()

Шаг 2: Измените MainActivity

Ваш класс MainActivity уже является владельцем жизненного цикла посредством наследования, поскольку суперкласс FragmentActivity реализует LifecycleOwner . Поэтому вам не нужно ничего делать, чтобы ваша активность учитывала жизненный цикл. Достаточно передать объект жизненного цикла активности в конструктор DessertTimer .

  1. Откройте MainActivity . В методе onCreate() измените инициализацию DessertTimer , включив this.lifecycle :
dessertTimer = DessertTimer(this.lifecycle)

Свойство lifecycle деятельности содержит объект Lifecycle , которым владеет эта деятельность.

  1. Удалите вызов startTimer() из onCreate() и вызов stopTimer() из onStop() . Вам больше не нужно сообщать DessertTimer , что делать в активности, поскольку DessertTimer теперь сам отслеживает жизненный цикл и автоматически уведомляется об изменении его состояния. Всё, что вы теперь делаете в этих обратных вызовах, — это регистрируете сообщение.
  2. Скомпилируйте и запустите приложение, а затем откройте Logcat. Обратите внимание, что таймер, как и ожидалось, начал работать.
  3. Нажмите кнопку «Домой», чтобы перевести приложение в фоновый режим. Обратите внимание, что таймер, как и ожидалось, остановился.

Что произойдёт с вашим приложением и его данными, если Android закроет его, пока оно работает в фоновом режиме? Этот сложный случай важно понимать.

Когда ваше приложение переходит в фоновый режим, оно не удаляется, а просто останавливается и ждёт, когда пользователь вернётся к нему. Но одна из главных задач ОС Android — обеспечить бесперебойную работу активного процесса. Например, если пользователь использует приложение GPS, чтобы сесть на автобус, важно быстро отобразить это приложение и продолжать показывать маршрут. Менее важно обеспечить бесперебойную работу приложения DessertClicker, которое пользователь, возможно, не открывал несколько дней, в фоновом режиме.

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

Иногда Android даже завершает весь процесс приложения, включая все связанные с ним действия. Android выполняет такое завершение работы, когда система испытывает нагрузку и существует риск визуального замедления, поэтому на этом этапе никакие дополнительные обратные вызовы или код не запускаются. Процесс вашего приложения просто завершается, незаметно, в фоновом режиме. Но для пользователя это не выглядит так, как будто приложение закрыто. Когда пользователь возвращается к приложению, которое ОС Android закрыла, Android перезапускает это приложение.

В этом задании вы смоделируете завершение процесса Android и изучите, что произойдет с вашим приложением при его повторном запуске.

Шаг 1: Используйте adb для имитации завершения процесса

Android Debug Bridge ( adb ) — это инструмент командной строки, позволяющий отправлять инструкции эмуляторам и устройствам, подключенным к вашему компьютеру. На этом этапе вы используете adb для завершения процесса приложения и смотрите, что произойдет, когда Android завершит работу приложения.

  1. Скомпилируйте и запустите приложение. Несколько раз нажмите на кекс.
  2. Нажмите кнопку «Домой», чтобы перевести приложение в фоновый режим. Теперь приложение остановлено и может быть закрыто, если Android потребуются ресурсы, которые оно использует.
  3. В Android Studio щелкните вкладку «Терминал» , чтобы открыть терминал командной строки.
  4. Введите adb и нажмите Return.

    Если вы видите много вывода, начинающегося с Android Debug Bridge version X.XX.X и заканчивающегося tags to be used by logcat (see logcat —h elp), всё в порядке. Если вместо этого вы видите adb: command not found , убедитесь, что команда adb доступна в вашем пути выполнения. Инструкции см. в разделе «Добавление adb в ваш путь выполнения» в главе «Утилиты» .
  5. Скопируйте и вставьте этот комментарий в командную строку и нажмите Enter:
adb shell am kill com.example.android.dessertclicker

Эта команда сообщает всем подключенным устройствам или эмуляторам о необходимости остановить процесс с именем пакета dessertclicker , но только если приложение находится в фоновом режиме. Поскольку приложение работало в фоновом режиме, на экране устройства или эмулятора не отображается никаких признаков остановки процесса. В Android Studio перейдите на вкладку « Выполнить» , чтобы увидеть сообщение «Приложение завершено». Перейдите на вкладку «Logcat» , чтобы увидеть, что обратный вызов onDestroy() не был выполнен — ваша активность просто завершилась.

  1. Используйте экран недавних, чтобы вернуться в приложение. Ваше приложение отображается в списке недавних независимо от того, переведено ли оно в фоновый режим или было полностью остановлено. При использовании экрана недавних для возвращения в приложение активность запускается снова. Активность проходит весь набор обратных вызовов жизненного цикла запуска, включая onCreate() .
  2. Обратите внимание, что при перезапуске приложение сбрасывает ваш «счёт» (как количество проданных десертов, так и общую сумму в долларах) до значений по умолчанию (0). Если Android закрыл приложение, почему оно не сохранило ваше состояние?

    Когда ОС перезапускает ваше приложение, Android делает всё возможное, чтобы сбросить его до прежнего состояния. Android сохраняет состояние некоторых представлений в пакете каждый раз, когда вы покидаете активность. Примерами автоматически сохраняемых данных являются текст в EditText (при наличии идентификатора в макете) и стек переходов вашей активности.

    Однако иногда ОС Android не располагает всеми вашими данными. Например, если в приложении DessertClicker есть пользовательская переменная, например, revenue , ОС Android не знает об этих данных и их важности для вашей активности. Вам необходимо добавить эти данные в пакет самостоятельно.

Шаг 2: Используйте onSaveInstanceState() для сохранения данных пакета

Метод onSaveInstanceState() — это обратный вызов, который используется для сохранения любых данных, которые могут понадобиться в случае, если ОС Android удалит ваше приложение. На диаграмме обратного вызова жизненного цикла onSaveInstanceState() вызывается после остановки активности. Он вызывается каждый раз, когда приложение переходит в фоновый режим.

Вызов onSaveInstanceState() можно рассматривать как меру безопасности: он даёт вам возможность сохранить небольшой объём информации в пакет, когда ваша активность выходит из активного режима. Система сохраняет эти данные сейчас, поскольку, если бы она ждала завершения работы приложения, ОС могла бы испытывать нехватку ресурсов. Сохранение данных каждый раз гарантирует, что данные об обновлениях в пакете будут доступны для восстановления при необходимости.

  1. В MainActivity переопределите обратный вызов onSaveInstanceState() и добавьте оператор журнала Timber .
override fun onSaveInstanceState(outState: Bundle) {
   super.onSaveInstanceState(outState)

   Timber.i("onSaveInstanceState Called")
}
  1. Скомпилируйте и запустите приложение, затем нажмите кнопку «Домой» , чтобы перевести его в фоновый режим. Обратите внимание, что обратный вызов onSaveInstanceState() происходит сразу после onPause() и onStop() :
  2. В верхней части файла, непосредственно перед определением класса, добавьте следующие константы:
const val KEY_REVENUE = "revenue_key"
const val KEY_DESSERT_SOLD = "dessert_sold_key"
const val KEY_TIMER_SECONDS = "timer_seconds_key"

Эти ключи будут использоваться как для сохранения, так и для извлечения данных из пакета состояния экземпляра.

  1. Прокрутите вниз до onSaveInstanceState() и обратите внимание на параметр outState , который имеет тип Bundle .

    Пакет — это набор пар «ключ-значение», где ключи всегда являются строками. В пакет можно помещать примитивные значения, такие как int и boolean значения.
    Поскольку система хранит этот пакет в оперативной памяти, рекомендуется сохранять данные в пакете небольшими. Размер этого пакета также ограничен, хотя он варьируется в зависимости от устройства. Как правило, следует хранить гораздо меньше 100 КБ, иначе приложение рискует аварийно завершить работу с ошибкой TransactionTooLargeException .
  2. В onSaveInstanceState() поместите значение revenue (целое число) в пакет с помощью метода putInt() :
outState.putInt(KEY_REVENUE, revenue)

Метод putInt() (и аналогичные методы из класса Bundle , такие как putFloat() и putString() принимает два аргумента: строку для ключа (константа KEY_REVENUE ) и фактическое значение для сохранения.

  1. Повторите тот же процесс с количеством проданных десертов и статусом таймера:
outState.putInt(KEY_DESSERT_SOLD, dessertsSold)
outState.putInt(KEY_TIMER_SECONDS, dessertTimer.secondsCount)

Шаг 3: Используйте onCreate() для восстановления данных пакета

  1. Прокрутите вверх до onCreate() и проверьте сигнатуру метода:
override fun onCreate(savedInstanceState: Bundle) {

Обратите внимание, что onCreate() получает Bundle при каждом вызове. При перезапуске активности из-за завершения процесса сохранённый вами Bundle передаётся в onCreate() . Если активность запускалась заново, этот Bundle в onCreate() равен null . Таким образом, если Bundle не null , вы знаете, что активность «пересоздаётся» с ранее известной точки.

  1. Добавьте этот код в onCreate() после настройки DessertTimer :
if (savedInstanceState != null) {
   revenue = savedInstanceState.getInt(KEY_REVENUE, 0)
}

Проверка на null определяет, есть ли данные в пакете или пакет равен null , что, в свою очередь, позволяет определить, было ли приложение запущено заново или было создано заново после завершения работы. Эта проверка является распространённым шаблоном для восстановления данных из пакета.

Обратите внимание, что используемый здесь ключ ( KEY_REVENUE ) — это тот же ключ, который вы использовали для putInt() . Чтобы убедиться, что вы используете один и тот же ключ каждый раз, рекомендуется определить эти ключи как константы. Для извлечения данных из пакета используется getInt() , так же как для помещения данных в пакет использовался putInt() . Метод getInt() принимает два аргумента:

  • Строка, которая действует как ключ, например, "key_revenue" для значения дохода.
  • Значение по умолчанию на случай, если для данного ключа в пакете не существует значения.

Целое число, которое вы получаете из пакета, затем присваивается переменной revenue , и пользовательский интерфейс будет использовать это значение.

  1. Добавьте методы getInt() для восстановления количества проданных десертов и значения таймера:
if (savedInstanceState != null) {
   revenue = savedInstanceState.getInt(KEY_REVENUE, 0)dessertsSold = savedInstanceState.getInt(KEY_DESSERT_SOLD, 0)
   dessertTimer.secondsCount =
       savedInstanceState.getInt(KEY_TIMER_SECONDS, 0)
}
  1. Скомпилируйте и запустите приложение. Нажмите на кекс не менее пяти раз, пока он не превратится в пончик. Нажмите «Домой», чтобы перевести приложение в фоновый режим.
  2. На вкладке Терминал Android Studio запустите adb , чтобы завершить процесс приложения.
adb shell am kill com.example.android.dessertclicker
  1. Используйте экран недавних покупок, чтобы вернуться в приложение. Обратите внимание, что на этот раз приложение отображается с правильными значениями выручки и количества проданных десертов из набора. Однако обратите внимание, что десерт снова стал кексом. Осталось сделать ещё кое-что, чтобы приложение восстанавливалось после закрытия точно в том же виде, в котором оно было.
  2. В MainActivity изучите метод showCurrentDessert() . Обратите внимание, что этот метод определяет, какое изображение десерта должно отображаться в активности, на основе текущего количества проданных десертов и списка десертов в переменной allDesserts .
for (dessert in allDesserts) {
   if (dessertsSold >= dessert.startProductionAmount) {
       newDessert = dessert
   }
    else break
}

Этот метод использует количество проданных десертов для выбора нужного изображения. Поэтому вам не нужно ничего делать для сохранения ссылки на изображение в бандле в onSaveInstanceState() . В этом бандле вы уже храните количество проданных десертов.

  1. В onCreate() , в блоке, восстанавливающем состояние из пакета, вызовите showCurrentDessert() :
 if (savedInstanceState != null) {
   revenue = savedInstanceState.getInt(KEY_REVENUE, 0)
   dessertsSold = savedInstanceState.getInt(KEY_DESSERT_SOLD, 0)
   dessertTimer.secondsCount = 
      savedInstanceState.getInt(KEY_TIMER_SECONDS, 0)
   showCurrentDessert()                   
}
  1. Скомпилируйте и запустите приложение, переведя его в фоновый режим. Используйте adb для завершения процесса. Чтобы вернуться в приложение, используйте экран недавних приложений. Обратите внимание, что значения количества десертов, общей выручки и изображения десерта восстановлены корректно.

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

Изменение конфигурации происходит, когда состояние устройства меняется настолько радикально, что самый простой способ для системы устранить это изменение — полностью завершить работу устройства и перестроить его. Например, если пользователь меняет язык устройства, может потребоваться изменить всю компоновку для адаптации к новому направлению текста. Если пользователь подключает устройство к док-станции или добавляет физическую клавиатуру, компоновке приложения может потребоваться использовать другой размер экрана или компоновку. А если меняется ориентация устройства (например, устройство поворачивается из портретной в альбомную или наоборот), может потребоваться изменить компоновку для соответствия новой ориентации.

Шаг 1: Изучите ротацию устройств и обратные вызовы жизненного цикла

  1. Скомпилируйте и запустите ваше приложение, затем откройте Logcat.
  2. Поверните устройство или эмулятор в альбомную ориентацию. Вы можете вращать эмулятор влево или вправо с помощью кнопок поворота или клавиш Control и стрелок ( Command и стрелок на Mac).
  3. Проверьте вывод в Logcat. Отфильтруйте вывод по MainActivity .
    Обратите внимание, что когда устройство или эмулятор поворачивает экран, система вызывает все обратные вызовы жизненного цикла для завершения активности. Затем, при повторном создании активности, система вызывает все обратные вызовы жизненного цикла для её запуска.
  4. В MainActivity закомментируйте весь метод onSaveInstanceState() .
  5. Скомпилируйте и запустите приложение ещё раз. Несколько раз нажмите на кекс и поверните устройство или эмулятор. На этот раз при повороте устройства и завершении и повторном создании активности она запустится со значениями по умолчанию.

    При изменении конфигурации Android использует тот же пакет состояния экземпляра, о котором вы узнали в предыдущей задаче, для сохранения и восстановления состояния приложения. Как и при завершении процесса, используйте onSaveInstanceState() для помещения данных приложения в пакет. Затем восстановите данные в onCreate() , чтобы избежать потери данных о состоянии активности при повороте устройства.
  6. В MainActivity раскомментируйте метод onSaveInstanceState() , запустите приложение, нажмите на кекс и поверните приложение или устройство. Обратите внимание, что на этот раз данные о десерте сохраняются при повороте активности.

Проект Android Studio: DessertClickerFinal

Советы по жизненному циклу

  • Если вы настраиваете или запускаете что-либо в обратном вызове жизненного цикла, остановите или удалите это действие в соответствующем обратном вызове. Остановив действие, вы гарантируете, что оно не продолжит выполняться, когда оно больше не понадобится. Например, если вы настраиваете таймер в onStart() , вам нужно приостановить или остановить его в onStop() .
  • Используйте onCreate() только для инициализации тех частей приложения, которые запускаются один раз при первом запуске приложения. Используйте onStart() для запуска тех частей приложения, которые запускаются как при запуске приложения, так и при каждом его возврате на передний план.

Библиотека жизненного цикла

  • Используйте библиотеку жизненного цикла Android, чтобы перенести управление жизненным циклом с активности или фрагмента на реальный компонент, который должен поддерживать жизненный цикл.
  • Владельцы жизненных циклов — это компоненты, которые имеют (и, следовательно, «владеют») жизненными циклами, включая Activity и Fragment . Владельцы жизненных циклов реализуют интерфейс LifecycleOwner .
  • Наблюдатели жизненного цикла отслеживают текущее состояние жизненного цикла и выполняют задачи при его изменении. Наблюдатели жизненного цикла реализуют интерфейс LifecycleObserver .
  • Объекты Lifecycle содержат фактические состояния жизненного цикла и запускают события при изменении жизненного цикла.

Чтобы создать класс, учитывающий жизненный цикл:

  • Реализуйте интерфейс LifecycleObserver в классах, которым необходимо поддерживать жизненный цикл.
  • Инициализируйте класс наблюдателя жизненного цикла с объектом жизненного цикла из активности или фрагмента.
  • В классе наблюдателя жизненного цикла аннотируйте методы, учитывающие жизненный цикл, интересующими их изменениями состояния жизненного цикла.

    Например, аннотация @OnLifecycleEvent(Lifecycle.Event.ON_START) указывает, что метод отслеживает событие жизненного цикла onStart .

Остановка процессов и сохранение состояния активности

  • Android регулирует работу фоновых приложений, чтобы обеспечить бесперебойную работу активного приложения. Это регулирование включает в себя ограничение объёма вычислений, выполняемых фоновыми приложениями, а иногда даже полное завершение процесса приложения.
  • Пользователь не может определить, закрыла ли система приложение в фоновом режиме. Приложение по-прежнему отображается на экране недавних приложений и должно перезапуститься в том же состоянии, в котором пользователь его оставил.
  • Android Debug Bridge ( adb ) — это инструмент командной строки, позволяющий отправлять инструкции эмуляторам и устройствам, подключенным к компьютеру. С помощью adb можно имитировать завершение процесса в приложении.
  • Когда Android завершает процесс вашего приложения, метод жизненного цикла onDestroy() не вызывается. Приложение просто останавливается.

Сохранение активности и состояния фрагмента

  • Когда приложение переходит в фоновый режим, сразу после вызова onStop() , данные приложения сохраняются в пакет. Некоторые данные приложения, например, содержимое EditText , сохраняются автоматически.
  • Bundle — это экземпляр Bundle , представляющий собой коллекцию ключей и значений. Ключи всегда являются строками.
  • Используйте функцию обратного вызова onSaveInstanceState() для сохранения других данных в пакете, которые вы хотите сохранить, даже если приложение было автоматически закрыто. Чтобы поместить данные в пакет, используйте методы пакета, начинающиеся с put , например, putInt() .
  • Получить данные из пакета можно в методе onRestoreInstanceState() или, что более распространено, в onCreate() . Метод onCreate() имеет параметр savedInstanceState , содержащий пакет.
  • Если переменная savedInstanceState содержит null , действие было запущено без пакета состояний и нет данных о состоянии для извлечения.
  • Чтобы извлечь данные из пакета с помощью ключа, используйте методы Bundle , которые начинаются с get , например getInt() .

Изменения конфигурации

  • Изменение конфигурации происходит, когда состояние устройства меняется настолько радикально, что самый простой способ для системы устранить изменение — это завершить работу и перестроить ее.
  • Наиболее распространённый пример изменения конфигурации — поворот устройства из портретного в альбомный режим или из альбомного в портретный. Изменение конфигурации также может происходить при смене языка устройства или подключении аппаратной клавиатуры.
  • При изменении конфигурации Android вызывает все обратные вызовы завершения жизненного цикла активности. Затем Android перезапускает активность с нуля, выполняя все обратные вызовы запуска жизненного цикла.
  • Когда Android закрывает приложение из-за изменения конфигурации, он перезапускает активность с пакетом состояний, доступным для onCreate() .
  • Как и в случае с завершением процесса, сохраните состояние вашего приложения в пакете в onSaveInstanceState() .

Курс Udacity:

Документация для разработчиков Android:

Другой:

В этом разделе перечислены возможные домашние задания для студентов, работающих над этой лабораторной работой в рамках курса, проводимого преподавателем. Преподаватель должен выполнить следующие действия:

  • При необходимости задавайте домашнее задание.
  • Объясните учащимся, как следует сдавать домашние задания.
  • Оцените домашние задания.

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

Если вы работаете с этой лабораторной работой самостоятельно, можете использовать эти домашние задания для проверки своих знаний.

Изменить приложение

Откройте приложение DiceRoller из урока 1. (Если у вас его нет, вы можете скачать приложение здесь .) Скомпилируйте и запустите приложение. Обратите внимание, что при повороте устройства текущее значение броска кубика теряется. Реализуйте onSaveInstanceState() , чтобы сохранить это значение в пакете, и восстановите его в onCreate() .

Ответьте на эти вопросы

Вопрос 1

Ваше приложение содержит физическую симуляцию, для отображения которой требуются сложные вычисления. Затем пользователю поступает телефонный звонок. Какое из следующих утверждений верно?

  • Во время телефонного разговора вам следует продолжать вычислять положения объектов в физической симуляции.
  • Во время телефонного разговора следует прекратить вычисление положений объектов в физической симуляции.

Вопрос 2

Какой метод жизненного цикла следует переопределить, чтобы приостановить моделирование, когда приложение не отображается на экране?

  • onDestroy()
  • onStop()
  • onPause()
  • onSaveInstanceState()

Вопрос 3

Какой интерфейс должен реализовывать класс, чтобы реализовать поддержку жизненного цикла с помощью библиотеки жизненного цикла Android?

  • Lifecycle
  • LifecycleOwner
  • Lifecycle.Event
  • LifecycleObserver

Вопрос 4

При каких обстоятельствах метод onCreate() в вашей активности получает Bundle с данными (то есть Bundle не null )? Возможны несколько вариантов ответа.

  • Действие возобновляется после поворота устройства.
  • Деятельность начинается с нуля.
  • Активность возобновляется после возвращения из фонового режима.
  • Устройство перезагружено.

Начните следующий урок: 5.1: ViewModel и ViewModelFactory

Ссылки на другие практические занятия по этому курсу см. на целевой странице практических занятий по основам Android Kotlin .