На этой странице вы узнаете о жизненном цикле составного объекта и о том, как 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 считает каждый сайт вызова отдельным. Вызов компонуемых объектов из нескольких мест вызова приведет к созданию нескольких экземпляров компонуемого объекта в композиции.
Если во время рекомпозиции составной объект вызывает другие составные объекты, чем во время предыдущей композиции, 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
в композиции при изменении состояния и возникновении рекомпозиции. Тот же цвет означает, что он не был перекомпонован.
Несмотря на то, что LoginInput
из первого вызова стал вторым, экземпляр LoginInput
будет сохраняться при рекомпозиции. Кроме того, поскольку у LoginInput
нет параметров, которые изменились бы при рекомпозиции, вызов LoginInput
будет пропущен Compose.
Добавьте дополнительную информацию для умной рекомпозиции.
Вызов составного объекта несколько раз также добавит его в композицию несколько раз. При вызове составного объекта несколько раз из одного и того же места вызова 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
в композиции при добавлении нового элемента в список. Составные элементы 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
в композиции при добавлении нового элемента в список. Поскольку составные элементы 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
и т. д. - Струны
- Все типы функций (лямбды)
Все эти типы могут следовать стабильному контракту, поскольку они неизменяемы. Поскольку неизменяемые типы никогда не меняются, им никогда не нужно уведомлять 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