На этой странице вы узнаете о жизненном цикле компонуемого объекта и о том, как Compose решает, нуждается ли компоновка компонуемого объекта в повторной композиции.
Обзор жизненного цикла
Как упоминалось в документации по управлению состоянием , композиция описывает пользовательский интерфейс вашего приложения и создается путем запуска компонуемых объектов. Композиция представляет собой древовидную структуру компонуемых объектов, которые описывают ваш пользовательский интерфейс.
Когда Jetpack Compose запускает ваши компонуемые элементы в первый раз, во время начальной композиции , он будет отслеживать компонуемые элементы, которые вы вызываете для описания вашего пользовательского интерфейса в композиции. Затем, когда состояние вашего приложения изменяется, Jetpack Compose планирует повторную композицию . Повторная композиция — это когда Jetpack Compose повторно выполняет компонуемые элементы, которые могли измениться в ответ на изменения состояния, а затем обновляет композицию, чтобы отразить любые изменения.
Композиция может быть создана только исходной композицией и обновлена перекомпозицией. Единственный способ изменить композицию — перекомпозицией.
Рисунок 1. Жизненный цикл компонуемого в композиции. Он входит в композицию, перекомпоновывается 0 или более раз и покидает композицию.
Перекомпозиция обычно запускается изменением объекта State<T>
. Compose отслеживает их и запускает все компонуемые объекты в Composition, которые считывают этот конкретный State<T>
, и любые компонуемые объекты, которые они вызывают и которые нельзя пропустить .
Если composable вызывается несколько раз, в Composition помещается несколько экземпляров. Каждый вызов имеет свой собственный жизненный цикл в Composition.
@Composable fun MyComposable() { Column { Text("Hello") Text("World") } }
Рисунок 2. Представление MyComposable
в Composition. Если composable вызывается несколько раз, в Composition помещается несколько экземпляров. Элемент, имеющий другой цвет, указывает на то, что это отдельный экземпляр.
Анатомия составного объекта в композиции
Экземпляр компонуемого в 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
composable и всегда вызывать LoginInput
composable. Каждый вызов имеет уникальный сайт вызова и исходную позицию, которые компилятор будет использовать для его уникальной идентификации.
Рисунок 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 использует порядок выполнения в дополнение к месту вызова, чтобы сохранить экземпляр отдельным в Composition. Если новый movie
добавляется в конец списка, Compose может повторно использовать экземпляры, уже находящиеся в Composition, поскольку их расположение в списке не изменилось, и, следовательно, входные данные movie
для этих экземпляров одинаковы.
Рисунок 4. Представление MoviesScreen
в Composition, когда новый элемент добавляется в конец списка. Компонуемые MovieOverview
в Composition можно использовать повторно. Одинаковый цвет в 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
, который ему передается. Если мы переупорядочиваем список фильмов, в идеале мы бы аналогичным образом переупорядочили экземпляры в дереве Composition вместо того, чтобы пересоставлять каждый компонуемый MovieOverview
с другим экземпляром фильма. Compose предоставляет вам способ сообщить среде выполнения, какие значения вы хотите использовать для идентификации заданной части дерева: key
компонуемый.
Обернув блок кода вызовом ключа composable с одним или несколькими переданными значениями, эти значения будут объединены для использования для идентификации этого экземпляра в композиции. Значение key
не обязательно должно быть глобально уникальным, оно должно быть уникальным только среди вызовов composable в месте вызова. Таким образом, в этом примере каждый movie
должен иметь key
, который является уникальным среди movies
; нормально, если он разделяет этот key
с каким-то другим composable в другом месте приложения.
@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
в items
DSL.
@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
и т. д. - Струны
- Все типы функций (лямбда)
Все эти типы способны следовать контракту stable, поскольку они неизменяемы. Поскольку неизменяемые типы никогда не меняются, им никогда не нужно уведомлять Composition об изменении, поэтому гораздо проще следовать этому контракту.
Один примечательный тип, который является стабильным, но изменяемым , — это тип 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