Жизненный цикл композиций

На этой странице вы узнаете о жизненном цикле composable-функции и о том, как Compose решает, нужна ли рекомпозиция.

Обзор жизненного цикла

Как указано в документации по управлению состоянием, композиция описывает интерфейс приложения и создается путем выполнения composable-функций. Композиция – это древовидная структура composable-функций, описывающих пользовательский интерфейс.

Когда Jetpack Compose запускает composable-функции в первый раз, во время начальной композиции, он отслеживает composable-функции, которые вы вызываете для описания пользовательского интерфейса в композиции. Затем, когда состояние приложения меняется, Jetpack Compose планирует рекомпозицию. Рекомпозиция – это когда Jetpack Compose повторно выполняет composable-функции, которые могли измениться в ответ на изменения состояния, а затем обновляет композицию, чтобы отразить любые изменения.

Композиция может быть создана только путем начальной композиции и обновлена путем рекомпозиции. The only way to modify a Composition is through recomposition.

Диаграмма, показывающая жизненный цикл composable-функции
Рисунок 1. Жизненный цикл composable-функции в композиции. Он входит в композицию, рекомпонуется ноль или более раз и покидает композицию.

Обычно рекомпозиция запускается при изменении объекта State<T>. Compose отслеживает их и запускает все composable-функции в композиции, которые считывают определенный объект State<T>, а также все composable-функции, которые они вызывают и которые нельзя пропустить.

Если composable-функция вызывается несколько раз, в композицию помещается несколько экземпляров. У каждого вызова в Composition свой жизненный цикл.

@Composable
fun MyComposable() {
    Column {
        Text("Hello")
        Text("World")
    }
}

Диаграмма, показывающая иерархическое расположение элементов в предыдущем фрагменте кода
Рисунок 2. Представление MyComposable в композиции. Если composable-функция вызывается несколько раз, в композицию помещается несколько экземпляров. Элемент другого цвета указывает на то, что это отдельный экземпляр.

Структура composable-функции в Composition

Экземпляр composable-функции в композиции идентифицируется по месту вызова. Компилятор Compose считает каждый сайт вызова отдельным. Если вызвать composable-функцию из нескольких мест, в композиции будет создано несколько ее экземпляров.

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

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

Пример:

@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 условно вызывает composable-функцию LoginError и всегда вызывает composable-функцию LoginInput. У каждого вызова есть уникальное место вызова и позиция источника, которые компилятор использует для его идентификации.

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

Несмотря на то, что LoginInput вызывается вторым, а не первым, экземпляр LoginInput будет сохранен при рекомпозиции. Кроме того, поскольку у LoginInput нет параметров, которые изменились в ходе рекомпозиции, вызов LoginInput будет пропущен Compose.

Добавьте дополнительную информацию, чтобы улучшить умное переписывание

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

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

В идеале идентификатор экземпляра MovieOverview должен быть связан с идентификатором movie, который ему передается. Если мы изменим порядок списка фильмов, в идеале мы должны аналогичным образом изменить порядок экземпляров в дереве композиции вместо того, чтобы перекомпоновывать каждую composable-функцию MovieOverview с другим экземпляром фильма. Compose позволяет указать среде выполнения, какие значения вы хотите использовать для идентификации определенной части дерева – composable-функции key.

Если обернуть блок кода вызовом composable-функции key с одним или несколькими переданными значениями, эти значения будут объединены и использованы для идентификации этого экземпляра в композиции. Значение key не должно быть глобально уникальным. Оно должно быть уникальным только среди вызовов composable-функций в месте вызова. В этом примере у каждого элемента movie должен быть уникальный ключ key, который не повторяется в movies. При этом у других composable-функций в приложении может быть такой же ключ 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 в композиции, когда в список добавляется новый элемент. Поскольку у composable-функций MovieOverview уникальные ключи, Compose распознает, какие экземпляры MovieOverview не изменились, и может использовать их повторно. Их побочные эффекты продолжат выполняться.

Некоторые composable-функции имеют встроенную поддержку composable-функции key. Например, LazyColumn позволяет указать специальный key в items DSL.

@Composable
fun MoviesScreenLazy(movies: List<Movie>) {
    LazyColumn {
        items(movies, key = { movie -> movie.id }) { movie ->
            MovieOverview(movie)
        }
    }
}

Пропуск, если входные данные не изменились

Во время рекомпозиции выполнение некоторых подходящих composable-функций может быть полностью пропущено, если их входные данные не изменились по сравнению с предыдущей композицией.

Пропуск composable-функции возможен, если:

  • Тип возвращаемого значения функции не Unit.
  • Функция аннотирована с помощью @NonRestartableComposable или @NonSkippableComposable.
  • Обязательный параметр имеет нестабильный тип

В компиляторе есть режим Strong Skipping, который смягчает последнее требование.

Чтобы тип считался стабильным, он должен соответствовать следующим требованиям:

  • Результат equals для двух экземпляров всегда будет одинаковым для одних и тех же двух экземпляров.
  • Если общедоступное свойство типа изменится, Composition получит уведомление.
  • Все общедоступные типы свойств также стабильны.

Есть несколько важных распространенных типов, которые подпадают под этот контракт и которые компилятор Compose будет рассматривать как стабильные, даже если они не помечены явно как стабильные с помощью аннотации @Stable:

  • Все типы примитивных значений: Boolean, Int, Long, Float, Char и т. д.
  • Струны
  • Все типы функций (лямбда-выражения)

Все эти типы могут следовать контракту стабильности, поскольку они неизменяемы. Поскольку неизменяемые типы никогда не меняются, им никогда не нужно уведомлять Composition об изменении, поэтому следовать этому контракту намного проще.

Один из примечательных типов, который является стабильным, но изменяемым, – это тип MutableState в Compose. Если значение хранится в MutableState, объект состояния в целом считается стабильным, поскольку Compose будет уведомлен о любых изменениях свойства .value в State.

Если все типы, переданные в качестве параметров в composable-функцию, стабильны, значения параметров сравниваются на равенство на основе позиции composable-функции в дереве пользовательского интерфейса. Рекомпозиция пропускается, если все значения не изменились с момента предыдущего вызова.

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