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

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

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

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

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

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

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