На этой странице вы узнаете о жизненном цикле компонуемого объекта и о том, как Compose определяет, требуется ли перекомпоновка компонуемого объекта.
Обзор жизненного цикла
Как упоминалось в документации по управлению состоянием , композиция описывает пользовательский интерфейс вашего приложения и создаётся путём запуска компонуемых объектов. Композиция представляет собой древовидную структуру компонуемых объектов, описывающих ваш пользовательский интерфейс.
Когда Jetpack Compose впервые запускает ваши компонуемые элементы во время первоначальной композиции , он отслеживает компонуемые элементы, вызываемые для описания пользовательского интерфейса в композиции. Затем, при изменении состояния приложения, Jetpack Compose планирует повторную композицию . При повторной композиции Jetpack Compose повторно выполняет компонуемые элементы, которые могли измениться в ответ на изменение состояния, а затем обновляет композицию, отражая все изменения.
Композиция может быть создана только путём первоначальной композиции и обновлена путём перекомпозиции. Единственный способ изменить композицию — это перекомпозиция.
Рисунок 1. Жизненный цикл компонуемого объекта в композиции. Он входит в композицию, перекомпоновывается 0 или более раз и покидает композицию.
Перекомпозиция обычно запускается изменением объекта State<T>
. Compose отслеживает эти изменения и запускает все компонуемые объекты в композиции, которые считывают это конкретное State<T>
, а также все вызываемые ими компонуемые объекты, которые нельзя пропустить .
Если компонуемый объект вызывается несколько раз, в композицию помещается несколько его экземпляров. Каждый вызов имеет свой собственный жизненный цикл в композиции.
@Composable fun MyComposable() { Column { Text("Hello") Text("World") } }
Рисунок 2. Представление MyComposable
в композиции. Если компонуемый элемент вызывается несколько раз, в композицию помещается несколько его экземпляров. Элемент, имеющий другой цвет, указывает на то, что это отдельный экземпляр.
Анатомия составного объекта в композиции
Экземпляр компонуемого объекта в Composition идентифицируется по месту его вызова . Компилятор Compose рассматривает каждое место вызова как отдельное. Вызов компонуемых объектов из нескольких мест вызова создаст несколько экземпляров компонуемого объекта в Composition.
Если во время повторной композиции составной объект вызывает другие составные объекты, чем во время предыдущей композиции, Compose определит, какие составные объекты были вызваны, а какие нет, а для тех составных объектов, которые были вызваны в обеих композициях, Compose не будет выполнять повторную композицию, если их входные данные не изменились .
Сохранение идентичности имеет решающее значение для связывания побочных эффектов с их компонуемыми компонентами, чтобы они могли успешно завершиться, а не перезапускаться при каждой перекомпозиции.
Рассмотрим следующий пример:
@Composable fun LoginScreen(showError: Boolean) { if (showError) { LoginError() } LoginInput() // This call site affects where LoginInput is placed in Composition } @Composable fun LoginInput() { /* ... */ } @Composable fun LoginError() { /* ... */ }
В приведённом выше фрагменте кода LoginScreen
будет вызывать компонуемый объект LoginError
при определённом условии и всегда будет вызывать компонуемый объект LoginInput
. Каждый вызов имеет уникальное место вызова и исходную позицию, которые компилятор будет использовать для его уникальной идентификации.
Рисунок 3. Отображение LoginScreen
в Composition при изменении состояния и выполнении перекомпоновки. Одинаковый цвет означает, что перекомпоновка не производилась.
Несмотря на то, что LoginInput
был вызван не первым, а вторым, экземпляр LoginInput
сохранится при рекомпозиции. Кроме того, поскольку у LoginInput
нет параметров, изменившихся при рекомпозиции, вызов LoginInput
будет пропущен Compose.
Добавьте дополнительную информацию для облегчения умных перекомпозиций
Многократный вызов компонуемого объекта также добавит его в Composition несколько раз. При многократном вызове компонуемого объекта из одной и той же точки вызова у Compose нет информации, позволяющей однозначно идентифицировать каждый вызов этого компонуемого объекта, поэтому для различения экземпляров используется порядок выполнения, а также точка вызова. Иногда такого поведения достаточно, но в некоторых случаях оно может привести к нежелательному поведению.
@Composable fun MoviesScreen(movies: List<Movie>) { Column { for (movie in movies) { // MovieOverview composables are placed in Composition given its // index position in the for loop MovieOverview(movie) } } }
В приведенном выше примере Compose использует порядок выполнения в дополнение к месту вызова, чтобы экземпляры были различимы в композиции. Если новый movie
добавляется в конец списка, Compose может повторно использовать экземпляры, уже находящиеся в композиции, поскольку их расположение в списке не изменилось, и, следовательно, входные данные movie
для них одинаковы.
Рисунок 4. Отображение MoviesScreen
в композиции при добавлении нового элемента в конец списка. Компонуемые объекты MovieOverview
в композиции можно использовать повторно. Одинаковый цвет в MovieOverview
означает, что компоновка объекта не была перекомпонована.
Однако если список movies
изменится, добавив новые элементы в начало или середину списка, удалив их или изменив порядок, это приведёт к перекомпоновке всех вызовов MovieOverview
, входной параметр которых изменил положение в списке. Это крайне важно, например, если MovieOverview
извлекает изображение фильма с помощью побочного эффекта. Если перекомпоновка происходит во время выполнения эффекта, она будет отменена и начнётся заново.
@Composable fun MovieOverview(movie: Movie) { Column { // Side effect explained later in the docs. If MovieOverview // recomposes, while fetching the image is in progress, // it is cancelled and restarted. val image = loadNetworkImage(movie.url) MovieHeader(image) /* ... */ } }
Рисунок 5. Отображение MoviesScreen
в Composition при добавлении нового элемента в список. Компонуемые объекты MovieOverview
не могут быть использованы повторно, и все побочные эффекты будут перезапущены. Другой цвет в MovieOverview
означает, что компонуемый объект был перекомпонован.
В идеале мы хотим представить себе, что идентификация экземпляра MovieOverview
связана с идентификацией переданного ему movie
. Если мы изменяем порядок в списке фильмов, в идеале мы аналогичным образом изменяем порядок экземпляров в дереве композиции, а не перекомпоновываем каждый компонуемый MovieOverview
с другим экземпляром фильма. Compose предоставляет возможность указать среде выполнения, какие значения вы хотите использовать для идентификации заданной части дерева: key
компонуемый объект.
При заключении блока кода в вызов ключевого компонуемого объекта с одним или несколькими переданными значениями эти значения будут объединены для использования в качестве идентификатора данного экземпляра в композиции. Значение key
не обязательно должно быть глобально уникальным, оно должно быть уникальным только среди вызовов компонуемых объектов в точке вызова. Таким образом, в этом примере каждый movie
должен иметь key
, уникальный среди movies
; допустимо, если он будет использовать этот key
совместно с каким-либо другим компонуемым объектом в другом месте приложения.
@Composable fun MoviesScreenWithKey(movies: List<Movie>) { Column { for (movie in movies) { key(movie.id) { // Unique ID for this movie MovieOverview(movie) } } } }
При использовании вышеизложенного, даже если элементы в списке изменяются, Compose распознает отдельные вызовы MovieOverview
и может использовать их повторно.
Рисунок 6. Представление MoviesScreen
в Composition при добавлении нового элемента в список. Поскольку компонуемые объекты MovieOverview
имеют уникальные ключи, Compose распознаёт, какие экземпляры MovieOverview
не изменились, и может использовать их повторно; их побочные эффекты продолжат выполняться.
Некоторые компонуемые элементы имеют встроенную поддержку key
. Например, LazyColumn
допускает указание пользовательского key
в DSL- items
.
@Composable fun MoviesScreenLazy(movies: List<Movie>) { LazyColumn { items(movies, key = { movie -> movie.id }) { movie -> MovieOverview(movie) } } }
Пропуск, если входные данные не изменились
Во время повторной композиции выполнение некоторых подходящих компонуемых функций может быть полностью пропущено, если их входные данные не изменились с предыдущей композиции.
Компонуемая функция может быть пропущена, если :
- Функция имеет не-
Unit
тип возвращаемого значения. - Функция аннотируется
@NonRestartableComposable
или@NonSkippableComposable
- Искомый параметр имеет нестабильный тип
Существует экспериментальный режим компилятора Strong Skipping , который смягчает последнее требование.
Для того чтобы тип считался стабильным, он должен соответствовать следующему контракту:
- Результат
equals
для двух экземпляров всегда будет одинаковым для одних и тех же двух экземпляров. - Если публичное свойство типа изменится, Composition будет уведомлен.
- Все виды общественной собственности также стабильны.
В этот контракт попадают некоторые важные общие типы, которые компилятор Compose будет рассматривать как стабильные, даже если они явно не помечены как стабильные с помощью аннотации @Stable
:
- Все примитивные типы значений:
Boolean
,Int
,Long
,Float
,Char
и т. д. - Струны
- Все типы функций (лямбда-функции)
Все эти типы могут следовать контракту стабильности, поскольку они неизменяемы. Поскольку неизменяемые типы никогда не меняются, им не нужно уведомлять композицию об изменении, поэтому следовать этому контракту гораздо проще.
Одним из примечательных типов, который является стабильным, но при этом изменяемым, является тип MutableState
класса Compose. Если значение хранится в MutableState
, объект состояния в целом считается стабильным, поскольку Compose будет уведомлен о любых изменениях свойства .value
объекта State
.
Если все типы, переданные в качестве параметров компонуемому объекту, стабильны, значения параметров сравниваются на предмет равенства в зависимости от позиции компонуемого объекта в дереве пользовательского интерфейса. Повторная композиция пропускается, если все значения не изменились с момента предыдущего вызова.
Compose считает тип стабильным только в том случае, если может это доказать. Например, интерфейс обычно считается нестабильным, а типы с изменяемыми публичными свойствами, реализация которых может быть неизменяемой, также нестабильны.
Если Compose не может определить, является ли тип стабильным, но вы хотите заставить Compose рассматривать его как стабильный, отметьте его аннотацией @Stable
.
// Marking the type as stable to favor skipping and smart recompositions. @Stable interface UiState<T : Result<T>> { val value: T? val exception: Throwable? val hasError: Boolean get() = exception != null }
В приведённом выше фрагменте кода, поскольку UiState
— это интерфейс, Compose обычно может считать этот тип нестабильным. Добавляя аннотацию @Stable
, вы сообщаете Compose, что этот тип стабилен, что позволяет Compose отдавать предпочтение умным рекомпозициям. Это также означает, что Compose будет считать все свои реализации стабильными, если интерфейс используется в качестве типа параметра.
Рекомендовано для вас
- Примечание: текст ссылки отображается, когда JavaScript отключен.
- State и Jetpack Compose
- Побочные эффекты в Compose
- Сохранение состояния пользовательского интерфейса в Compose