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

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

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