05-lifecycle.md 21 KB

Жизненный цикл активности

Оглавление

Введение

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

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

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

Состояния жизненного цикла

Активность имеет следующие состояния жизненного цикла:

  • Created — активность создана, находится в фоне и пользователь еще не может с ней взаимодействовать.
  • Started/Resumed — активность запущена и находится на переднем плане приложения, пользователь может с ней взаимодействовать.
  • Paused — активность запущена и находится на заднем плане, пользователь не может с ней взаимодействовать, т.е. активность перекрыта всплывающим окном или диалогом.
  • Stopped — активность находится в фоне и не отображается на экране и пользователь не может с ней взаимодействовать.
  • Destroyed — активность уничтожена.

Для отслеживания состояний жизненного цикла каждая активность (и фрагменты) имеет специальные методы-callback'и (у фрагментов названия методов могут отличаться, см. изображение выше): onCreate(), onStart(), onResume(), onPause(), onStop(), onDestroy().

Метод onCreate():

onCreate() - первый метод, с которого начинается выполнение активности. В этом методе активность переходит в состояние Created. Он вызывается непосредственно после создания активности. Здесь осуществляется инициализация интерфейса пользователя, привязка данных к элементам интерфейса, создание потоков и т.д.

Метод onStart():

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

Метод onResume():

При вызове метода onResume() активность переходит в состояние Resumed, отображается на экране и пользователь может с ней взаимодействовать. До тех пор пока активность не потеряет фокус, она находится в этом состоянии. Фокус активность может потерять вследствие переключения пользователя на другую активность или из-за выключения экрана устройства.

Метод onPause():

Метод onPause() вызывается, когда пользователь переходит от одной активности к другой. В этом методе можно освобождать используемые ресурсы, приостанавливать процессы, например, воспроизведение аудио, анимаций. После вызова этого метода активность становится невидимой, не отображается на экране, но она все еще активна. Если пользователь решит вернуться к этой активности, то система вызовет метод onResume(), и активность снова появится на экране.

Метод onStop():

Метод onStop() переводит активность в состояние Stopped. Метод освобождает неиспользуемые ресурсы, которые использует активность. При этом сама активность остается в памяти устройства и состояние всех элементов интерфейса сохраняется. Если пользователь решит вернуться к активности, также будет вызван метод onResume() и активность будет отображена на экране.

Метод onDestroy():

Метод onDestroy() вызывается, когда активность завершает свою работу. Метод может быть вызван, либо, когда система решает "убить" активность, если системе не хватает ресурсов, либо, если пользователь "убивает" приложение самостоятельно. Также метод onDestroy() вызывается при смене ориентации устройства, а затем активность создается заново, вызывая метод onCreate().

Логирование состояний жизненного цикла

Для наглядной проверки вызовов методов-callback'ов жизненного цикла будет использоваться логирование.

Логирование заключается в выводе служебных сообщений в лог приложения. Для логирования в Android используется класс Log.

Для работы необходимо открыть проект "Dessert Pusher", перейти в класс MainActivity в метод onCreate() и добавить вызов метода Log.i().

Log.i("MainActivity", "onCreate called")

Первым параметром указывается тег лог-сообщения, по которому его можно будет найти. Вторым параметром указывается непосредственно сообщение. После запуска приложения сообщение можно найти во вкладке Logcat на нижней панели Android Studio.

Далее добавить методы onStart(), onResume(), onPause(), onStop() и onDestroy() и добавить вывод лог-сообщения аналогичным образом.

После запуска приложения можно убедиться, что будут вызваны методы onCreate() -> onStart() -> onResume().

Если нажать на системную кнопку "Назад" и закрыть приложение, будут вызваны onPause() -> onStop() -> onDestroy(). Метод onDestroy() вызывается поскольку пользователь технически закрыл приложение нажав на кнопку "Назад". Если же вместо системной кнопки "Назад" нажать на кнопку "Домой", то метод onDestroy() вызван не будет до тех пор, пока системе Android не потребуются дополнительные ресурсы.

Если при работе приложения нажать на кнопку "Поделиться" на Action Bar, то будет вызван метод onPaused() — активность запущена, но пользователь не может с ней взаимодействовать, поскольку на переднем плане отображается диалоговое окно. Если его закрыть, то на активности будет вызван метод onResume. Метод onPause() вызывается, когда активность теряет фокус, метод onResume() — когда активность получает фокус.

Управление таймером в рамках жизненного цикла

Методы-callback'и жизненного цикла требуются для возможности управления длительными процессами, которые нельзя прерывать при сворачивании или закрытии приложения. Так, если приложение поддерживает воспроизведение видео-файлов, то оно должно корректно обрабатывать ситуации закрытия, сворачивания приложения, входящего звонка и т.п. Например, в методе onPause() приложение должно ставить воспроизведение видео на паузу, в методе onStop() останавливать воспроизведение.

В качестве простейшего примера будет рассмотрено управление таймером, который раз в секунду печатает сообщение в лог среды разработки. Проект "Dessert Timer" содержит класс DessertTimer, который и является таймером с методами startTimer() и stopTimer().

Первое, что нужно сделать — раскомментировать весь код файла DessertTimer.kt. Далее, заменить использование вызова Timber.i() на Log.i(). Timber — это класс одноименной библиотеки логирования. Она позволяет упростить процедуру логирования, однако в данном конспекте библиотека рассмотрена не будет.

Метод startTimer() запускает работу объекта Handler, который выполняет описанный код в объекте Runnable на отдельном потоке выполнения раз в 1 секунду. Код в Runnable выполняет увеличение счетчика секунд работы таймера и вывод в лог сообщения о числе прошедших секунд.

Далее необходимо добавить объект DessertTimer в MainActivity, добавить запуск таймера в метод onStart() и остановку в метод onStop().

private lateinit var dessertTimer: DessertTimer

...

override fun onCreate(savedInstanceState: Bundle?) {
    super.onCreate(savedInstanceState)
    Log.i("MainActivity", "onCreate called")
    dessertTimer = DessertTimer()
    ...
}

override fun onStart() {
    super.onStart()
    Log.i("MainActivity", "onStart called")
    dessertTimer.startTimer()
}

override fun onStop() {
    super.onStop()
    Log.i("MainActivity", "onStop called")
    dessertTimer.stopTimer()
}

После запуска приложения таймер запускается и в лог выводятся соответствующие сообщения. Если нажать на кнопку "Share" в Action Bar, то таймер продолжает работать, поскольку на активности вызван только метод onPause(). Если же свернуть приложение, нажав на системную кнопку "Home", то по логам видно, что таймер останавливается. Если запустить приложение заново, то таймер продолжает свою работу, продолжит выводить сообщения в лог и увеличивать счетчик секунд. Если же выйти из приложения, нажав системную кнопку "Назад", то приложение будет остановлено и "уничтожено" (вызван метод onDestroy()), таймер соответственно остановится. Если запустить приложение снова, то ход таймера начнется полностью заново, поскольку будет вызван метод onCreate(), где экземпляр таймера будет полностью создан заново.

Lifecycle Library

В 2017-м году команда Android-разработчиков представила на Google I/O библиотеку Lifecycle Library, которая упрощает работу с методами-callback'ами жизненного цикла и в целом работу с жизненным циклом активностей и фрагментов.

Основная идея библиотеки заключается в том, что управлением состоянием объектов, должны заниматься не активности и фрагменты, имеющие методы-callback'и, а сами объекты, состояние которых должно меняться в зависимости от текущего статуса жизненного цикла.

Для изучения использования библиотеки рекомендуется пройти урок 4 "Activity & Fragment Lifecycle": https://classroom.udacity.com/courses/ud9012/lessons/e487c600-ed68-4576-a35a-12f211cf032e/concepts/527dc024-4431-4ad0-a567-2512eca12b45.

Документация: https://developer.android.com/topic/libraries/architecture/lifecycle.

Сохранение состояние приложения

Приложение "Dessert Timer" является кликером, по нажатию на "вкусняшку" на экране увеличивается счетчик проданных и полученная сумма денег. Если выйти из приложения по нажатию на системную кнопку "Назад" и открыть приложение снова, то можно заметить, что счетчики количества и суммы денег сбрасываются в 0. По сути эти счетчики являются прогрессом приложения и этот прогресс хотелось бы сохранить значения счетчиков в ситуации, когда система Android сама уничтожила приложение по причине необходимости ресурсов или при смене ориентации экрана (в этой ситуации активность также уничтожается и создается заново).

Для сохранения состояния каждой активности перед ее остановкой используется метод onSaveInstanceState(), а для восстановления состояния после запуска используется onRestoreInstanceState(). Эти методы также являются callback'ами жизненного цикла активности. Метод onSaveInstanceState() вызывается после onStop(), если версия Android устройства Android P (API 28) или выше, и перед onStop(), если API менее 28. Метод onRestoreInstanceState() вызывается после onStart().

Далее будет рассмотрена реализация сохранения и восстановления состояния активности MainActivity.

1. Реализации сохранения активности:

override fun onSaveInstanceState(outState: Bundle) {
    super.onSaveInstanceState(outState)
    Log.i("MainActivity", "onSaveInstanceState called")
    outState.putInt("key_revenue", revenue)
    outState.putInt("key_dessertsSold", dessertsSold)
}

Метод onSaveInstanceState() принимает на вход стандартный объект Bundle, куда и необходимо сохранить все значения, которые должны быть восстановлены позднее. Класс Bundle имеет методы put для различных типов и классов, позволяющие хранить значения и ключи для их восстановления.

2. Реализация восстановления состояния:

Реализовать восстановление состояния помимо метода onRestoreInstanceState() можно еще и в методе onCreate(). Метод onCreate() вызывается раньше, чем метод onRestoreInstanceState(), поэтому восстанавливать состояние активности в нем может быть более эффективным и удобным, т.к. именно в onCreate() выполняется инициализация всех объектов графических компонентов и полей класса. Метод onCreate() имеет в качестве параметра объект savedInstanceState типа Bundle?. Главное — проверить объект на null перед попыткой получения значений.

override fun onCreate(savedInstanceState: Bundle?) {
	...

    if (savedInstanceState != null) {
        revenue = savedInstanceState.getInt("key_revenue")
        dessertsSold = savedInstanceState.getInt("key_dessertsSold")
    }
    binding.revenue = revenue
    binding.amountSold = dessertsSold
    ...
}

Метод onCreate() имеет код с установкой значений для текстовых полей revenue и abountSold и восстановление значений необходимо добавлять именно перед установкой этих значений.

Если запустить приложение, накликать некоторые значения и выполнить смену ориентации, то во время смены ориентации будут вызваны и onSaveInstanceState() и onCreate(), значения счетчиков будут сохранены и восстановлены.

Стоит отметить, что нажатие на системную кнопку "Назад" не вызывает метода сохранения onSaveInstanceState(). Поэтому для сохранения состояния приложения при перезапуске приложения необходимо сохранять данные либо в настройках, либо в локальной базе данных.