Концепции и реализация Jetpack Compose
Компоненты, учитывающие жизненный цикл, выполняют действия в ответ на изменение статуса жизненного цикла другого компонента, например, активности или фрагмента. Эти компоненты помогают создавать более организованный и зачастую более легковесный код, который легче поддерживать.
Распространенный подход заключается в реализации действий зависимых компонентов в методах жизненного цикла активностей и фрагментов. Однако такой подход приводит к плохой организации кода и увеличению количества ошибок. Используя компоненты, учитывающие жизненный цикл, вы можете перенести код зависимых компонентов из методов жизненного цикла в сами компоненты.
Пакет androidx.lifecycle предоставляет классы и интерфейсы, позволяющие создавать компоненты, учитывающие жизненный цикл , — компоненты, которые могут автоматически корректировать свое поведение в зависимости от текущего состояния жизненного цикла активности или фрагмента.
Большинство компонентов приложения, определенных в Android Framework, имеют привязанные к ним жизненные циклы. Жизненные циклы управляются операционной системой или кодом фреймворка, работающим в вашем процессе. Они являются основой работы Android, и ваше приложение должно их соблюдать. Несоблюдение может привести к утечкам памяти или даже сбоям приложения.
Представьте, что у нас есть активность, которая отображает местоположение устройства на экране. Типичная реализация может выглядеть следующим образом:
Котлин
internal class MyLocationListener(
private val context: Context,
private val callback: (Location) -> Unit
) {
fun start() {
// connect to system location service
}
fun stop() {
// disconnect from system location service
}
}
class MyActivity : AppCompatActivity() {
private lateinit var myLocationListener: MyLocationListener
override fun onCreate(...) {
myLocationListener = MyLocationListener(this) { location ->
// update UI
}
}
public override fun onStart() {
super.onStart()
myLocationListener.start()
// manage other components that need to respond
// to the activity lifecycle
}
public override fun onStop() {
super.onStop()
myLocationListener.stop()
// manage other components that need to respond
// to the activity lifecycle
}
}
Java
class MyLocationListener {
public MyLocationListener(Context context, Callback callback) {
// ...
}
void start() {
// connect to system location service
}
void stop() {
// disconnect from system location service
}
}
class MyActivity extends AppCompatActivity {
private MyLocationListener myLocationListener;
@Override
public void onCreate(...) {
myLocationListener = new MyLocationListener(this, (location) -> {
// update UI
});
}
@Override
public void onStart() {
super.onStart();
myLocationListener.start();
// manage other components that need to respond
// to the activity lifecycle
}
@Override
public void onStop() {
super.onStop();
myLocationListener.stop();
// manage other components that need to respond
// to the activity lifecycle
}
}
Хотя этот пример выглядит неплохо, в реальном приложении в итоге получается слишком много вызовов, управляющих пользовательским интерфейсом и другими компонентами в зависимости от текущего состояния жизненного цикла. Управление несколькими компонентами размещает значительный объем кода в методах жизненного цикла, таких как onStart() и onStop , что затрудняет их поддержку.
Более того, нет гарантии, что компонент запустится до остановки активности или фрагмента. Это особенно актуально, если нам нужно выполнить длительную операцию, например, проверку конфигурации в onStart . Это может привести к состоянию гонки, когда метод onStop() завершается раньше, чем onStart , поддерживая компонент в рабочем состоянии дольше, чем это необходимо.
Котлин
class MyActivity : AppCompatActivity() {
private lateinit var myLocationListener: MyLocationListener
override fun onCreate(...) {
myLocationListener = MyLocationListener(this) { location ->
// update UI
}
}
public override fun onStart() {
super.onStart()
Util.checkUserStatus { result ->
// what if this callback is invoked AFTER activity is stopped?
if (result) {
myLocationListener.start()
}
}
}
public override fun onStop() {
super.onStop()
myLocationListener.stop()
}
}
Java
class MyActivity extends AppCompatActivity {
private MyLocationListener myLocationListener;
public void onCreate(...) {
myLocationListener = new MyLocationListener(this, location -> {
// update UI
});
}
@Override
public void onStart() {
super.onStart();
Util.checkUserStatus(result -> {
// what if this callback is invoked AFTER activity is stopped?
if (result) {
myLocationListener.start();
}
});
}
@Override
public void onStop() {
super.onStop();
myLocationListener.stop();
}
}
Пакет androidx.lifecycle предоставляет классы и интерфейсы, которые помогают решать эти проблемы отказоустойчивым и изолированным способом.
Жизненный цикл
Класс Lifecycle хранит информацию о состоянии жизненного цикла компонента (например, активности или фрагмента) и позволяет другим объектам отслеживать это состояние.
Для отслеживания статуса Lifecycle цикла соответствующего компонента используются два основных перечисления:
Событие
События жизненного цикла, которые отправляются из фреймворка и класса Lifecycle . Эти события сопоставляются с событиями обратного вызова в активностях и фрагментах.
Состояние
Текущее состояние компонента, отслеживаемое объектом Lifecycle .
Представьте состояния как узлы графа, а события — как ребра между этими узлами.
Класс может отслеживать статус жизненного цикла компонента, реализуя интерфейс DefaultLifecycleObserver и переопределяя соответствующие методы, такие как onCreate, onStart и т. д. Затем вы можете добавить наблюдателя, вызвав метод addObserver() класса Lifecycle и передав экземпляр вашего наблюдателя, как показано в следующем примере:
Котлин
class MyObserver : DefaultLifecycleObserver {
override fun onResume(owner: LifecycleOwner) {
connect()
}
override fun onPause(owner: LifecycleOwner) {
disconnect()
}
}
myLifecycleOwner.getLifecycle().addObserver(MyObserver())
Java
public class MyObserver implements DefaultLifecycleObserver {
@Override
public void onResume(LifecycleOwner owner) {
connect()
}
@Override
public void onPause(LifecycleOwner owner) {
disconnect()
}
}
myLifecycleOwner.getLifecycle().addObserver(new MyObserver());
В приведенном выше примере объект myLifecycleOwner реализует интерфейс LifecycleOwner , который описан в следующем разделе.
Владелец жизненного цикла
Интерфейс LifecycleOwner имеет один метод, указывающий на наличие у класса Lifecycle . Он содержит один метод, getLifecycle , который должен быть реализован самим классом. Если же вы пытаетесь управлять жизненным циклом всего процесса приложения, см. ProcessLifecycleOwner .
Этот интерфейс абстрагирует владение Lifecycle от отдельных классов, таких как Fragment и AppCompatActivity , и позволяет писать компоненты, работающие с ними. Любой пользовательский класс приложения может реализовать интерфейс LifecycleOwner .
Компоненты, реализующие интерфейс DefaultLifecycleObserver беспрепятственно взаимодействуют с компонентами, реализующими интерфейс LifecycleOwner поскольку владелец может указать жизненный цикл, за которым может зарегистрироваться наблюдатель.
В примере с отслеживанием местоположения мы можем сделать так, чтобы класс MyLocationListener реализовывал интерфейс DefaultLifecycleObserver , а затем инициализировать его с помощью Lifecycle активности в методе onCreate() . Это позволяет классу MyLocationListener быть самодостаточным, то есть логика реагирования на изменения статуса жизненного цикла объявляется в MyLocationListener , а не в активности. Хранение собственной логики отдельными компонентами упрощает управление логикой активности и фрагментов.
Котлин
class MyActivity : AppCompatActivity() {
private lateinit var myLocationListener: MyLocationListener
override fun onCreate(...) {
myLocationListener = MyLocationListener(this, lifecycle) { location ->
// update UI
}
Util.checkUserStatus { result ->
if (result) {
myLocationListener.enable()
}
}
}
}
Java
class MyActivity extends AppCompatActivity {
private MyLocationListener myLocationListener;
public void onCreate(...) {
myLocationListener = new MyLocationListener(this, getLifecycle(), location -> {
// update UI
});
Util.checkUserStatus(result -> {
if (result) {
myLocationListener.enable();
}
});
}
}
Один из распространенных сценариев использования — это предотвращение вызова определенных коллбэков, если Lifecycle в данный момент находится в неоптимальном состоянии. Например, если коллбэк запускает транзакцию фрагмента после сохранения состояния активности, это приведет к сбою, поэтому мы никогда не захотим вызывать этот коллбэк.
Для упрощения этого варианта использования класс Lifecycle позволяет другим объектам запрашивать текущее состояние.
Котлин
internal class MyLocationListener(
private val context: Context,
private val lifecycle: Lifecycle,
private val callback: (Location) -> Unit
): DefaultLifecycleObserver {
private var enabled = false
override fun onStart(owner: LifecycleOwner) {
if (enabled) {
// connect
}
}
fun enable() {
enabled = true
if (lifecycle.currentState.isAtLeast(Lifecycle.State.STARTED)) {
// connect if not connected
}
}
override fun onStop(owner: LifecycleOwner) {
// disconnect if connected
}
}
Java
class MyLocationListener implements DefaultLifecycleObserver {
private boolean enabled = false;
public MyLocationListener(Context context, Lifecycle lifecycle, Callback callback) {
...
}
@Override
public void onStart(LifecycleOwner owner) {
if (enabled) {
// connect
}
}
public void enable() {
enabled = true;
if (lifecycle.getCurrentState().isAtLeast(STARTED)) {
// connect if not connected
}
}
@Override
public void onStop(LifecycleOwner owner) {
// disconnect if connected
}
}
Благодаря этой реализации наш класс LocationListener полностью учитывает жизненный цикл. Если нам нужно использовать LocationListener из другой активности или фрагмента, нам достаточно просто инициализировать его. Все операции инициализации и завершения управляются самим классом.
Если библиотека предоставляет классы, которые должны работать с жизненным циклом Android, мы рекомендуем использовать компоненты, учитывающие жизненный цикл. Клиенты вашей библиотеки смогут легко интегрировать эти компоненты без ручного управления жизненным циклом на стороне клиента.
Реализация пользовательского владельца жизненного цикла
В библиотеках поддержки версий 26.1.0 и более поздних уже реализованы фрагменты и действия, использующие интерфейс LifecycleOwner .
Если у вас есть собственный класс, который вы хотите сделать владельцем LifecycleOwner , вы можете использовать класс LifecycleRegistry , но вам необходимо передавать события в этот класс, как показано в следующем примере кода:
Котлин
class MyActivity : Activity(), LifecycleOwner {
private lateinit var lifecycleRegistry: LifecycleRegistry
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
lifecycleRegistry = LifecycleRegistry(this)
lifecycleRegistry.markState(Lifecycle.State.CREATED)
}
public override fun onStart() {
super.onStart()
lifecycleRegistry.markState(Lifecycle.State.STARTED)
}
override fun getLifecycle(): Lifecycle {
return lifecycleRegistry
}
}
Java
public class MyActivity extends Activity implements LifecycleOwner {
private LifecycleRegistry lifecycleRegistry;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
lifecycleRegistry = new LifecycleRegistry(this);
lifecycleRegistry.markState(Lifecycle.State.CREATED);
}
@Override
public void onStart() {
super.onStart();
lifecycleRegistry.markState(Lifecycle.State.STARTED);
}
@NonNull
@Override
public Lifecycle getLifecycle() {
return lifecycleRegistry;
}
}
Рекомендации по использованию компонентов с учетом жизненного цикла
- Старайтесь максимально упростить контроллеры пользовательского интерфейса (активности и фрагменты). Они не должны пытаться получать собственные данные; вместо этого используйте для этого
ViewModelи отслеживайте изменения в объектеLiveData, чтобы отражать их в представлениях. - Старайтесь создавать пользовательские интерфейсы, управляемые данными, где контроллер пользовательского интерфейса отвечает за обновление представлений по мере изменения данных или за уведомление
ViewModelо действиях пользователя. - Разместите логику обработки данных в классе
ViewModel.ViewModelдолжен служить связующим звеном между контроллером пользовательского интерфейса и остальной частью приложения. Однако будьте осторожны:ViewModelне отвечает за получение данных (например, из сети). Вместо этогоViewModelдолжен вызывать соответствующий компонент для получения данных, а затем передавать результат обратно контроллеру пользовательского интерфейса. - Используйте привязку данных (Data Binding) , чтобы поддерживать чистый интерфейс между вашими представлениями и контроллером пользовательского интерфейса. Это позволит сделать ваши представления более декларативными и минимизировать код обновления, который вам нужно писать в ваших активностях и фрагментах. Если вы предпочитаете делать это на языке программирования Java, используйте библиотеку, такую как Butter Knife, чтобы избежать шаблонного кода и получить лучшую абстракцию.
- Если ваш пользовательский интерфейс сложен, рассмотрите возможность создания класса- презентера для обработки изменений интерфейса. Это может быть трудоемкой задачей, но она упростит тестирование компонентов вашего интерфейса.
- Избегайте ссылок на контекст
ViewилиActivityв вашейViewModel. ЕслиViewModelсуществует дольше, чем активность (в случае изменений конфигурации), ваша активность будет иметь утечку памяти и не будет должным образом освобождена сборщиком мусора. - Используйте сопрограммы Kotlin для управления длительными задачами и другими операциями, которые могут выполняться асинхронно.
Варианты использования компонентов, учитывающих жизненный цикл
Компоненты, учитывающие жизненный цикл, могут значительно упростить управление жизненными циклами в самых разных ситуациях. Вот несколько примеров:
- Переключение между точным и детальным обновлением местоположения. Используйте компоненты, учитывающие жизненный цикл, чтобы включить детальное обновление местоположения, когда ваше приложение для определения местоположения находится в режиме видимости, и переключиться на точный режим обновления, когда приложение находится в фоновом режиме.
LiveData, компонент, учитывающий жизненный цикл, позволяет вашему приложению автоматически обновлять пользовательский интерфейс при изменении местоположения пользователя. - Остановка и запуск буферизации видео. Используйте компоненты, учитывающие жизненный цикл, чтобы начать буферизацию видео как можно скорее, но отложить воспроизведение до полного запуска приложения. Вы также можете использовать компоненты, учитывающие жизненный цикл, чтобы завершить буферизацию при уничтожении приложения.
- Запуск и остановка сетевого подключения. Используйте компоненты, учитывающие жизненный цикл, чтобы обеспечить обновление (потоковую передачу) сетевых данных в режиме реального времени, пока приложение находится на переднем плане, а также автоматическую приостановку работы приложения при переходе в фоновый режим.
- Приостановка и возобновление работы анимированных изображений. Используйте компоненты, учитывающие жизненный цикл, для обработки приостановки работы анимированных изображений, когда приложение находится в фоновом режиме, и возобновления работы изображений после того, как приложение перейдет в активный режим.
Обработка событий остановки
Когда Lifecycle принадлежит AppCompatActivity или Fragment , состояние Lifecycle изменяется на CREATED , и событие ON_STOP отправляется при вызове onSaveInstanceState() объекта AppCompatActivity или Fragment .
Когда состояние Fragment или AppCompatActivity сохраняется с помощью onSaveInstanceState , его пользовательский интерфейс считается неизменяемым до вызова ON_START . Попытка изменить пользовательский интерфейс после сохранения состояния, скорее всего, приведет к несоответствиям в состоянии навигации вашего приложения, поэтому FragmentManager выбрасывает исключение, если приложение запускает FragmentTransaction после сохранения состояния. Подробности см. в commit() .
LiveData предотвращает этот частный случай по умолчанию, воздерживаясь от вызова своего наблюдателя, если связанный с ним Lifecycle не достиг хотя бы STARTED . Внутри системы перед принятием решения о вызове наблюдателя вызывается метод isAtLeast() .
К сожалению, метод onStop() класса AppCompatActivity вызывается после onSaveInstanceState , что создает лазейку, в которой изменения состояния пользовательского интерфейса не допускаются, но Lifecycle еще не переведен в состояние CREATED .
Чтобы предотвратить эту проблему, в версии beta2 и ниже класс Lifecycle помечает состояние как CREATED без отправки события, так что любой код, проверяющий текущее состояние, получает реальное значение, даже если событие не отправляется до тех пор, пока система не вызовет метод onStop() .
К сожалению, у этого решения есть две основные проблемы:
- Начиная с API уровня 23 и ниже, система Android фактически сохраняет состояние активности, даже если она частично занята другой активностью. Другими словами, система Android вызывает
onSaveInstanceState(), но не обязательно вызываетonStop. Это создает потенциально длительный интервал, в течение которого наблюдатель по-прежнему считает, что жизненный цикл активен, даже если состояние пользовательского интерфейса изменить нельзя. - Любой класс, желающий реализовать поведение, аналогичное классу
LiveData, должен использовать обходное решение, предусмотренное в бета-версииLifecyclebeta 2и более ранних версиях.
Дополнительные ресурсы
Чтобы узнать больше об управлении жизненными циклами с помощью компонентов, учитывающих жизненный цикл, обратитесь к следующим дополнительным ресурсам.
Образцы
- Sunflower — демонстрационное приложение, показывающее лучшие практики использования архитектурных компонентов.