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

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

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

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

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

Композицию можно создать только на основе исходной композиции и обновить с помощью повторной композиции. Изменить композицию можно только путем ее повторного создания.

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

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

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

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

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

Анатомия composable-функции в композиции

Экземпляр композиции в Composition определяется по месту вызова. Компилятор Compose рассматривает каждый сайт вызова как отдельный. При вызове composable-функций из нескольких мест вызова в композиции будет создано несколько экземпляров 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. Добавляется компонуемый элемент LoginError, но другие компонуемые элементы не перекомпоновываются.
Рисунок 3. Представление LoginScreen в композиции при изменении состояния и повторной композиции. Одинаковый цвет означает, что изображение не было перекомпоновано.

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

Как добавить дополнительную информацию для интеллектуальной перекомпоновки

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

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

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

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

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

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

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

Компонуемая функция может быть пропущена, если:

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

В режиме компилятора Пропуск с проверкой последнее требование не действует.

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

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

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

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

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

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

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

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