Понимание жизненного цикла Android и изменения его состояния имеет решающее значение для создания приложений с меньшим количеством ошибок, использующих меньше ресурсов и обеспечивающих хорошее взаимодействие с пользователем.
Когда дело доходит до создания приложения на Android, активити и фрагменты являются ключевыми компонентами для создания пользовательского интерфейса (UI). Когда пользователь перемещается по приложению, эти компоненты проходят через разные состояния жизненного цикла Android.
Понимание жизненного цикла и правильная реакция на изменения его состояния имеют решающее значение. Он позволяет создавать приложения с меньшим количеством ошибок, использовать меньше системных ресурсов Android и обеспечивать хорошее взаимодействие с пользователем.
В этом туториале вы познакомитесь с простым приложением PuppyCounter, которое можно использовать для подсчета собак во время прогулки по окрестностям. Вы узнаете:
- Роль жизненного цикла в Android приложениях.
- Основы жизненных циклов активити и фрагментов, а также колбэки (обратные вызовы), вызываемые, когда действие перемещается между состояниями жизненного цикла.
- Как сохранить и восстановить состояние экземпляра активити.
- Процесс передачи данных между активити и фрагментами.
- Как ViewModel может помочь вам хранить и управлять данными, связанными с пользовательским интерфейсом с учетом жизненного цикла.
Для этого урока вам понадобится Android Studio и Android-устройство или эмулятор.
Начнем
Для начала загрузите материалы. Откройте стартовый проект в Android Studio. Как только проект откроется, дайте ему запуститься и синхронизироваться, и вы будете готовы к работе!
Запустите приложение и проверьте его возможности:
В приложении два экрана:
- Главный экран: позволяет подсчитывать собак разных размеров. Вы можете нажимать на карточки или на кнопки «плюс» и «минус», чтобы обновить счетчики. На верхней панели у вас есть две кнопки: первая сбрасывает значения, а вторая открывает экран «Share» (Поделиться).
- Экран Share: показывает количество ваших собак. Нажмите на «Share», чтобы открыть диалоговое окно, в котором вас спросят, хотите ли вы поделиться своим счетом. В этом туториале вы не реализуете эту логику, но можете притвориться, что она существует. :]
Затем ознакомьтесь со структурой проекта:
Как видите, много, что для вас уже приготовлено. Самые важные пакеты - это активити и фрагменты. Вы будете перемещаться между ними, когда узнаете об активити и жизненном цикле фрагмента. Прямо сейчас не беспокойтесь о деталях. Вы познакомитесь с классами внутри по мере прохождения данного туториала. Помимо этих пакетов, обратите внимание на следующие три вещи:
- DogCount: класс модели для ваших данных.
- SplashActivity: активити, которая открывается при запуске приложения.
- PuppyCounterApplication: класс приложения.
Прежде чем вдаваться в подробности жизненного цикла активити, рассмотрим некоторую предысторию роли жизненного цикла в приложениях Android.
Понимание роли жизненного цикла приложений
Операционная система (ОС) Android - это многопользовательская система Linux. В большинстве случаев каждое приложение работает в собственном процессе Linux. ОС создает процесс, когда необходимо выполнить любой из компонентов приложения. Когда ни один компонент приложения не запущен, а ОС требуется освободить память для запуска других приложений, процесс прерывается.
ОС Android использует иерархию по важности, чтобы определить, какие процессы оставить в живых или уничтожить. В этой иерархии процессы делятся на разные типы. Этот тип зависит от запущенных в данный момент компонентов приложения и их текущего состояния.
Самый распространенный компонент приложения - Activity. Каждое приложение для Android имеет одно или несколько активити. Когда пользователь перемещается по приложению, действия проходят через разные состояния жизненного цикла.
Разработчики должны понимать, как различные компоненты влияют на время жизни процесса. Неправильное использование этих компонентов может привести к тому, что система остановит процесс, пока выполняет важную работу.
Изучение жизненного цикла активности
На рисунке выше показаны различные состояния, через которые происходит активность в течение своего жизненного цикла:
- Initialized: создается экземпляр активити и инициализируются его свойства.
- Created: активити теперь полностью инициализировано и готово к настройке своего пользовательского интерфейса.
- Started: активити видна пользователю.
- Resumed: активити видна пользователю и находится в фокусе. В этом состоянии пользователь, скорее всего, взаимодействует с активити.
- Destroyed: активити уничтожена, и ОС может освободить свою память.
Обратите внимание на разные колбэки (обратные вызовы) между состояниями. ОС вызывает эти колбэки, когда активити переходит из одного состояния в другое. Вы можете переопределить эти методы в своих активити, чтобы выполнять задачи в ответ на эти изменения состояния жизненного цикла.
Прежде чем объяснять каждый колбэк, проверьте их в действии. Откройте MainActivity.kt и добавьте логирование в onCreate() и onResume(). Также переопределите другие колбэки:
override fun onStart() {
Timber.i("PuppyCounter - MainActivity - onStart()")
super.onStart()
}
override fun onCreate(savedInstanceState: Bundle?) {
Timber.i("PuppyCounter - MainActivity - onCreate()")
super.onCreate(savedInstanceState)
setContentView(R.layout.layout_main)
findViews()
setupSmallDogViewsClickListeners()
setupMiddleDogViewsClickListeners()
setupBigDogViewsClickListeners()
}
override fun onResume() {
Timber.i("PuppyCounter - MainActivity - onResume()")
super.onResume()
renderDogCount(dogCount)
}
override fun onPause() {
Timber.i("PuppyCounter - MainActivity - onPause()")
super.onPause()
}
override fun onStop() {
Timber.i("PuppyCounter - MainActivity - onStop()")
super.onStop()
}
override fun onDestroy() {
Timber.i("PuppyCounter - MainActivity - onDestroy()")
super.onDestroy()
}
Всякий раз, когда вы переопределяете такой колбэк, убедитесь, что вы также вызываете метод суперкласса. Если вы этого не сделаете, ваше приложение не выполнит какую-то важную работу и может вылететь или зависнуть.
Скомпилируйте и запустите приложение. Затем проверьте логи. Чтобы просмотреть логи в Android Studio, откройте инструмент Logcat, щелкнув Logcat внизу на странице. Введите PuppyCounter в историю поиска, чтобы показать результаты:
Затем закройте приложение, нажав назад, или проведите пальцем в обратном направлении, если у вас включена навигация с помощью жестов. Еще раз проверьте логи. Вы должны увидеть что-то вроде этого:
Заметка
В логах вы не увидите сообщение «Back button clicked» (Нажата кнопка назад). Мы добавили это в изображение, чтобы вам было легче заметить нажатия кнопок.
Понимание колбэков жизненного цикла активити
Вы только что прошли один полный жизненный цикл активити: активити была создана, возобновлена и окончательно уничтожена, когда вы вышли из приложения.
На приведенной выше диаграмме представлен жизненный цикл активити:
- onCreate(): активити переходит в состояние Создано. Здесь вы выполняете логику, которая должна выполняться только один раз за все время активити. Сюда можно включить настройку контент вью, связывание активити с ViewModel, создание экземпляров некоторых переменных области класса и т.д.
- onStart(): активити переходит в состояние Начато. Этот вызов делает активити видимой для пользователя, поскольку приложение готовится к тому, чтобы активити перешла на передний план и стала интерактивной.
- onResume(): активити переходит в состояние Возобновлено. Теперь пользователь может взаимодействовать с активити. Здесь вы можете включить любую функциональность, которая должна работать, пока компонент виден и находится на переднем плане.
- onPause(): активити переходит в состояние Приостановлено. Этот вызов указывает на то, что активити больше не находится на переднем плане, хотя она может быть видна, например, если пользователь находится в многооконном режиме. В это время вам следует приостановить или настроить операции, которые не должны продолжаться или должны продолжаться в модерации. Действие остается в этом состоянии до возобновления активити, например, открытия или закрытия нижнего листа в действии, или до тех пор, пока оно не станет полностью невидимым для пользователя, например, при открытии другого действия.
- onStop(): активити переходит в состояние Остановлено. Активити больше не видна пользователю. Здесь вы должны освободить или настроить ресурсы, которые не нужны, пока активити не видна пользователю. Вы также должны использовать эту возможность для выполнения операций выключения для задач, которые относительно интенсивно загружают процессор, например, для операций с базой данных.
- onDestroy(): активити переходит в состояние Уничтожено. На этом работа заканчивается. Это может быть потому, что:
- Пользователь полностью закрывает активити.
- finish() вызывается для активити.
- Система временно прерывает активити из-за изменения конфигурации, например поворота устройства или многооконного режима.
Бывают ситуации, когда система убивает процесс, следовательно, не вызывает onDestroy() или любые другие методы жизненного цикла активити. Таким образом, его не следует использовать для того, чтобы делать те вещи, которые должны остаться после завершения процесса.
Заметка
Дополнительные сведения о жизненном цикле Activity смотрите в Android Developer documentation.
Сохранение и восстановление состояния экземпляра активити
Если вы немного поиграли с приложением, то возможно, заметили пару ошибок. Счетчик увеличивается при нажатии карточек на главном экране.
Теперь поверните устройство, чтобы изменить ориентацию экрана. Если на дисплее вашего устройства включен автоповорот, вы увидите что-то вроде этого:
Состояние счетчика обнулилось при смене ориентации экрана. Посмотрим логи:
Вы можете видеть, что когда поменялась ориентация экрана, приложение уничтожило активити в портретной ориентации, а затем создало и возобновило новую активити в горизонтальной ориентации. Поскольку в MainActivity.kt у вас нет никакой логики для сохранения и восстановления состояния счетчика, действие было потеряно во время этого процесса.
Скоро мы это исправим!
Сохранение состояния экземпляра
Откройте MainActivity.kt и добавьте следующий код:
override fun onSaveInstanceState(outState: Bundle) {
Timber.i("PuppyCounter - MainActivity - onSaveInstanceState()")
// Save the dog count state
outState.putParcelable(STATE_DOG_COUNT, dogCount)
// Always call the superclass so it can save the view hierarchy state
super.onSaveInstanceState(outState)
}
Когда активити начинает останавливаться, ОС вызывает onSaveInstanceState(), чтобы действие сохранило любую информацию о состоянии в бандле сохранения состояния. Некоторые вью Android обрабатывают по умолчанию - EditText для текста и ListView для позиции прокрутки.
Заметка
OnSaveInstanceState() не вызывается, когда пользователь явно закрывает активити или когда вызывается finish().
Чтобы сохранить состояние dogCount, вы переопределили onSaveInstanceState() и сохранили состояние в Bundle как пару ключ-значение с помощью outState.putParcelable(). В качестве ключа вы использовали STATE_DOG_COUNT, который уже был определен в классе.
Проверьте класс DogCount. Вы заметите, что он реализует Parcelable. Если вы не знакомы с Parcelable - это интерфейс, концептуально похожий Serializable в Java. Классы, реализующие Parcelable, могут быть записаны и восстановлены из Parcel, который разработан для высокопроизводительного транспорта IPC (IPC transport). Проще говоря, он позволяет хранить простые структуры данных в Bundle.
Восстановление состояния
Прекрасно! Теперь у вас есть логика для сохранения состояния, но в ней нет пользы, пока у вас нет логики для ее получения. В MainActivity.kt добавьте следующий код ниже onSaveInstanceState():
override fun onRestoreInstanceState(savedInstanceState: Bundle) {
Timber.i("PuppyCounter - MainActivity - onRestoreInstanceState()")
// Always call the superclass so it can restore the view hierarchy
super.onRestoreInstanceState(savedInstanceState)
dogCount = savedInstanceState.getParcelable(STATE_DOG_COUNT) ?: DogCount()
}
Любое состояние, которое вы сохраняете в onSaveInstanceState(), вы можете восстановить в onRestoreInstanceState(). onRestoreInstanceState() получает Bundle, который содержит пары ключ-значение, которые вы можете прочитать. Здесь вы использовали saveInstanceState.getParcelable() для получения состояния DogCount. Обратите внимание, что вы использовали тот же ключ, что и для сохранения состояния: STATE_DOG_COUNT.
Заметка
ОС вызывает onRestoreInstanceState() после обратного вызова onStart(), только если у нее есть сохраненное состояние для восстановления. Вы также можете восстановить состояние в onCreate(), потому что этот колбэк получает тот же Bundle.
Запустите приложение. Увеличьте значения счетчиков и поменяйте ориентацию экрана:
Также проверьте логи, когда все колбеки будут вызваны, вы увидите следующую картину:
Заметка
Не путайте onSaveInstanceState() и onRestoreInstanceState() с колбеками жизненного цикла активности. ОС вызывает эти методы только в тот момент, когда это нужно.
Отлично! Теперь, когда вы исправили ошибку в приложении, пора перейти к следующей. :)
Передача данных между экранами
Увеличьте значения счетчиков на главном экране, а затем откройте экран «Share». Вы заметите, что значения экрана "Share" не совпадают со значениями на главном экране.
В MainActivity.kt измените showShareScreen() следующим образом:
private fun showShareScreen() {
Timber.i("PuppyCounter - MainActivity - start ShareActivity Intent")
val intent = ShareActivity.createIntent(this)
// Store DogCount state to the intent
intent.putExtra(ShareActivity.EXTRA_DOG_COUNT, dogCount)
startActivity(intent)
}
С помощью этого кода вы сохраняете состояние DogCount в Intent. Здесь вы используете подход, аналогичный тому, что вы видели в предыдущем разделе. Да, этим вы передадите данные в ShareActivity, но вам все равно нужно добавить логику для их получения.
В ShareActivity.kt добавьте следующий метод:
private fun readExtras() = intent.extras?.run {
Timber.i("PuppyCounter - ShareActivity - readExtras()")
dogCount = getParcelable(EXTRA_DOG_COUNT) ?: DogCount()
}
Этот метод извлекает объект Intent, который запустил это действие, и пытается получить дополнительные данные, которые были переданы с ним. В этом конкретном случае он попытается получить состояние DogCount.
Чтобы завершить логику получения, вызовите этот метод в onCreate() в ShareActivity.kt:
override fun onCreate(savedInstanceState: Bundle?) {
Timber.i("PuppyCounter - ShareActivity - onCreate()")
super.onCreate(savedInstanceState)
setContentView(R.layout.layout_share)
findViews()
// Read extra data from the Intent
readExtras()
setOnShareBtnClickListener()
}
При получении данных из Intent лучше всего это сделать в onCreate(). Таким образом, у вас будет время настроить состояние до того, как действие возобновится и пользователь начнет с ним взаимодействовать.
Супер! Запустите приложение. Увеличьте значения счетчиков и откройте экран «Share». Вы увидите что-то вроде этого:
Проверьте логи, чтобы увидеть жизненные циклы активити при переходе от одного экрана к другому.
Обратите внимание, как ОС создает ShareActivity сразу после вызова onPause() MainActivity. Как упоминалось ранее, приложение вызывает onStop(), когда активити больше не видна пользователю. После вызова onPause() MainActivity вы можете увидеть серию колбэков жизненного цикла ShareActivity, которые включают чтение intent данных. После возобновления ShareActivity полностью виден пользователю, и может быть вызвана функция MainActivity onStop(), за которой следует onSaveInstanceState().
Разбираем данные объекта Intent
Измените ориентацию экрана на экране Share и обратите внимание на то, что происходит. Вы увидите, что приложение сохранило состояние dogCount. Как это возможно, если вы не реализовали логику для сохранения и получения состояния экземпляра?
Проверяйте логи! :)
Вы уже знакомы с тем, как состояние может быть потеряно во время изменения конфигурации. В этом случае обратите внимание на то, что лог readExtras() снова присутствует, когда приложение создает новый ShareActivity. Но если вы проверите код, вы увидите, что вы распечатываете этот лог, только если intent.extras отличается от null - или, другими словами, Intent содержит некоторые данные.
Данные, которые вы передаете с помощью Intent при запуске новой активити, сохраняются при воссоздании активити.
Чтобы завершить этот раздел, нажмите назад, когда экран находится в горизонтальной ориентации, и еще раз просмотрите логи.
ShareActivity приостановлен, а старая портретная активность MainActivity уничтожена. Затем создается и возобновляется новая горизонтальная MainActivity. Наконец, приложение вызывает onStop() и onDestroy() ShareActivity.
Отлично! Теперь, когда вы понимаете жизненный цикл активности и то, как правильно управлять состоянием активити, пора переходить к фрагментам. :)
Изучение жизненного цикла фрагмента
Как и у активити, у фрагментов есть свой жизненный цикл. Когда пользователь перемещается по вашему приложению и взаимодействует с ним, ваши фрагменты переходят из одного состояния в другое в своем жизненном цикле, когда они добавляются, удаляются, а также выходят на экран или выходят из него.
На рисунке выше вы можете видеть, что жизненный цикл фрагмента аналогичен жизненному циклу активити, но содержит некоторые дополнительные методы, специфичные для фрагмента. Прежде чем объяснять каждый колбэк, проверьте их в приложении.
В предыдущем разделе вы поиграли с двумя активити и увидели, как меняется их жизненный цикл при перемещении между экранами. В этом примере вы реализуете те же экраны с фрагментами. Вы можете найти два фрагмента, которые представляют каждый экран в пакете фрагментов: MainFragment.kt и ShareFragment.kt. Также есть одна активити контейнера и пакет viewmodels. Пока не обращайте внимания на пакет viewmodels. Он понадобится вам в следующем разделе.
Если вы проверите MainFragment.kt, вы заметите много общего с MainActivity.kt. У них одинаковая логика управления состояниями, но MainFragment.kt содержит еще несколько колбэков жизненного цикла.
Перед запуском приложения откройте SplashActivity.kt и обновите startFirstActivity(), чтобы он запускал ActivityWithFragments вместо MainActivity:
private fun startFirstActivity() {
startActivity(Intent(this, ActivityWithFragments::class.java))
}
Отлично! Теперь соберите и запустите приложение. Затем осмотрите логи.
Обратите внимание, как жизненный цикл фрагмента синхронизируется с жизненным циклом активности. Сначала приложение создает и запускает ActivityWithFragments. После этого он создает и запускает фрагмент и его просмотр. Наконец, он возобновляет как активити, так и фрагмент.
Далее нажмите назад и снова наблюдайте за логами.
Закрыв приложение, вы запустили процесс уничтожения активити. Как и раньше, события жизненного цикла фрагмента следуют за событиями жизненного цикла активити. И активити, и фрагмент сначала приостанавливаются, затем останавливаются и, наконец, уничтожаются.
Состояние жизненного цикла фрагмента никогда не может быть выше, чем у его родительского. Например, родительский фрагмент или активити должны начинаться до его дочерних фрагментов. Точно так же дочерние фрагменты должны останавливаться до их родительского фрагмента или активности. Из приведенных выше логов можно подумать наоборот - что сначала останавливается действие, но это только потому, что вы распечатываете логи как первый вызов в колбэках жизненного цикла. Внутренне ОС обеспечивает остановку всех дочерних фрагментов перед остановкой активити.
Понимание обратных вызовов жизненного цикла фрагмента
Теперь вы можете немного глубже погрузиться в каждое событие жизненного цикла, чтобы лучше понять жизненный цикл фрагмента:
- onCreate(): фрагмент достигает состояния Created. Подобно onCreate() активити, этот колбэк получает Bundle, содержащий любое состояние, ранее сохраненное onSaveInstanceState().
- onCreateView(): вызывается для расширения или создания вью фрагмента.
- onViewCreated(): вью фрагмента создается с ненулевым объектом View. Это представление устанавливается для фрагмента и может быть получено с помощью getView().
- onStart(): фрагмент переходит в состояние Started. В этом состоянии гарантируется, что вью фрагмента доступно и безопасно выполнить FragmentTransaction для дочернего FragmentManager фрагмента.
- onResumed(): фрагмент переходит в состояние Resumed. Он становится видимым после завершения всех эффектов Animator и Transition. Теперь пользователь может взаимодействовать с фрагментом.
- onPause(): фрагмент возвращается в состояние Started. ОС вызывает этот колбэк, когда пользователь начинает покидать фрагмент, пока фрагмент все еще виден.
- onStop(): фрагмент возвращается в состояние Created и больше не отображается.
- onDestroyView(): запускается после завершения всех анимаций выхода и переходов, когда представление фрагмента было отделено от окна. На этом этапе все ссылки на представление фрагмента должны быть удалены, что позволит очистить представление фрагмента от мусора.
- onDestroy(): фрагмент переходит в состояние Destroyed. Это происходит при удалении фрагмента или при уничтожении FragmentManager. На этом этапе жизненный цикл фрагмента подошел к концу.
Теперь, когда вы лучше понимаете, что скрывается под капотом, переходите между основным экраном и экраном Share, чтобы увидеть танец жизненного цикла фрагмента. :)
Как вы видели в этом и предыдущем разделе, жизненный цикл Android довольно сложен. Управлять состояниями и взаимодействовать с пользовательским интерфейсом в нужное время может быть непросто для неопытных разработчиков. Это привело к появлению некоторых новых API и компонентов Android, которые должны облегчить жизнь всем разработчикам Android. Одним из таких компонентов является ViewModel.
Использование ViewModel для хранения данных пользовательского интерфейса
ViewModel предназначен для хранения данных, связанных с пользовательским интерфейсом, и управления ими с учетом жизненного цикла.
Во-первых, замените логику сохранения и восстановления состояния в MainFragment.kt подходом, использующим ViewModel.
В пакете viewmodels создайте новый класс с именем MainViewModel.kt.
import androidx.lifecycle.LiveData
import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.ViewModel
import com.raywenderlich.android.puppycounter.model.DogCount
class MainViewModel : ViewModel() {
private val _dogCount: MutableLiveData by lazy {
MutableLiveData(DogCount())
}
val dogCount: LiveData = _dogCount
fun setDogCount(dogCount: DogCount) {
_dogCount.value = dogCount
}
}
Это будет ViewModel для вашего главного экрана. Вы будете использовать его для сохранения состояния DogCount. Используйте _dogCount для отслеживания состояния и dogCount для отображения его наблюдателям. Для обновления состояния используйте setDogCount().
Если вы хотите узнать больше о LiveData, обратитесь к Android Developer documentation.
В MainFragment.kt добавьте следующее:
import androidx.fragment.app.viewModels
import com.raywenderlich.android.puppycounter.fragments.viewmodels.MainViewModel
Добавьте следующую строку выше onCreate():
private val viewModel: MainViewModel by viewModels()
Таким образом, вы добавили код для создания MainViewModel для MainFragment.
Затем добавьте в MainFragment.kt следующий метод:
private fun subscribeToViewModel() {
viewModel.dogCount.observe(viewLifecycleOwner, { value ->
dogCount = value
renderDogCount(dogCount)
})
}
Этот метод позволяет вам подписаться на наблюдаемое состояние в MainViewModel. Каждый раз, когда состояние dogCount изменяется, приложение передает новое состояние во вью, и с новым состоянием вызывается renderDogCount(), в результате чего пользовательский интерфейс обновляется.
Затем измените onViewCreated(), чтобы подписаться на MainViewModel после вызова суперкласса:
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
Timber.i("PuppyCounter - MainFragment - onViewCreated()")
super.onViewCreated(view, savedInstanceState)
subscribeToViewModel()
findViews(view)
setupSmallDogViewsClickListeners(view)
setupMiddleDogViewsClickListeners(view)
setupBigDogViewsClickListeners(view)
}
Вам также понадобится логика, которая обновит состояние в MainViewModel. Измените updateDogCount() следующим образом:
private fun updateDogCount(newDogCount: DogCount) {
viewModel.setDogCount(newDogCount)
}
Этот метод вызывается всякий раз, когда пользователь обновляет счетчики. Он обновит MainViewModel с новым состоянием. MainViewModel протолкнет это состояние через dogCount, и MainFragment получит уведомление, поскольку он подписан на MainViewModel.
Наконец, в MainFragment удалите onSaveInstanceState(), вызов renderDogCount (dogCount) из onResume() и код saveInstanceState? .Run {...} в onCreate(). Вам это больше не нужно. :)
Скомпилируйте и запустите приложение. Коснитесь счетчиков пару раз и поверните экран. Вы должны увидеть что-то вроде этого:
На приведенном ниже рисунке можно увидеть время жизни ViewModel рядом с соответствующим жизненным циклом действия.
Объекты ViewModel привязаны к Lifecycle, передаваемому ViewModelProvider при получении ViewModel. Он остается в памяти до тех пор, пока Lifecycle, на который он рассчитан, не исчезнет навсегда. В случае активити это происходит, когда она завершена. Для фрагмента это происходит, когда он отсоединяется.