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

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

Диаграмма, показывающая, как предыдущий код перекомпоновывается, если флаг showError изменяется на true. Добавляется компонуемый LoginError, но другие компонуемые не перекомпоновываются.

Рисунок 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 и может использовать их повторно.

Диаграмма, показывающая, как предыдущий код перекомпоновывается, если новый элемент добавляется в начало списка. Поскольку элементы списка идентифицируются ключами, Compose знает, что не нужно перекомпоновывать их, даже если их позиции изменились.

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

{% дословно %} {% endverbatim %} {% дословно %} {% endverbatim %}