Жизненный цикл составных компонентов

На этой странице вы узнаете о жизненном цикле компонуемого объекта и о том, как 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 . Каждый вызов имеет уникальное место вызова и позицию источника, которые компилятор будет использовать для его однозначной идентификации.

Диаграмма, показывающая, как перекомпоновывается приведенный выше код, если флаг showError изменен на true. Добавляется компонент LoginError, но остальные компоненты не перекомпоновываются.
Рисунок 3. Отображение LoginScreen в систему в композиции при изменении состояния и перекомпозиции. Один и тот же цвет означает, что перекомпозиция не производилась.

Несмотря на то, что вызов 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 в композиции при добавлении нового элемента в список. Компоненты 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 и может использовать их повторно.

Диаграмма, показывающая, как перекомпоновывается приведенный выше код, если в начало списка добавляется новый элемент. Поскольку элементы списка идентифицируются по ключам, Compose знает, что их не нужно перекомпоновывать, даже если их позиции изменились.
Рисунок 6. Представление MoviesScreen в композиции при добавлении нового элемента в список. Поскольку компонуемые объекты 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 будет рассматривать все свои реализации как стабильные, если в качестве типа параметра используется интерфейс.

{% verbatim %} {% endverbatim %} {% verbatim %} {% endverbatim %}