Как и большинство других наборов инструментов пользовательского интерфейса, Compose визуализирует кадр через несколько отдельных фаз . Например, система Android View имеет три основные фазы: измерение, макет и рисование. Compose очень похож, но имеет важную дополнительную фазу, называемую композицией в начале.
В документации Compose композиция описывается в разделах Thinking in Compose and State и Jetpack Compose .
Три фазы кадра
Сочинение состоит из трех основных этапов:
- Composition : Какой пользовательский интерфейс показывать. Compose запускает компонуемые функции и создает описание вашего пользовательского интерфейса.
- Макет : Где разместить UI. Эта фаза состоит из двух шагов: измерение и размещение. Элементы макета измеряют и размещают себя и любые дочерние элементы в 2D-координатах для каждого узла в дереве макета.
- Рисование : как это отображается. Элементы пользовательского интерфейса рисуются на холсте, обычно на экране устройства.

Порядок этих фаз, как правило, одинаков, что позволяет данным течь в одном направлении от композиции к макету и рисованию для создания кадра (также известно как однонаправленный поток данных ). BoxWithConstraints
, LazyColumn
и LazyRow
являются заметными исключениями, где состав его дочерних элементов зависит от фазы макета родительского элемента.
Концептуально каждая из этих фаз происходит для каждого кадра; однако для оптимизации производительности Compose избегает повторной работы, которая вычисляла бы те же результаты из тех же входных данных во всех этих фазах. Compose пропускает запуск компонуемой функции, если он может повторно использовать предыдущий результат, а Compose UI не перерисовывает или не перерисовывает все дерево, если в этом нет необходимости. Compose выполняет только минимальный объем работы, необходимый для обновления UI. Такая оптимизация возможна, поскольку Compose отслеживает считывания состояния в разных фазах.
Понять фазы
В этом разделе более подробно описывается, как выполняются три фазы компоновки для компонуемых объектов.
Состав
На этапе композиции среда выполнения Compose выполняет компонуемые функции и выводит древовидную структуру, которая представляет ваш UI. Это дерево UI состоит из узлов макета, которые содержат всю информацию, необходимую для следующих этапов, как показано в следующем видео:
Рисунок 2. Дерево, представляющее ваш пользовательский интерфейс, созданное на этапе композиции.
Подраздел кода и дерева пользовательского интерфейса выглядит следующим образом:

В этих примерах каждая компонуемая функция в коде отображается на один узел макета в дереве пользовательского интерфейса. В более сложных примерах компонуемые могут содержать логику и поток управления и создавать другое дерево с учетом разных состояний.
Макет
На этапе макета Compose использует дерево пользовательского интерфейса, созданное на этапе композиции, в качестве входных данных. Коллекция узлов макета содержит всю информацию, необходимую для принятия решения о размере и расположении каждого узла в 2D-пространстве.
Рисунок 4. Измерение и размещение каждого узла макета в дереве пользовательского интерфейса на этапе макета.
На этапе компоновки обход дерева выполняется с использованием следующего трехшагового алгоритма:
- Измерение дочерних элементов : Узел измеряет свои дочерние элементы, если таковые имеются.
- Определите свой размер : на основе этих измерений узел определяет свой размер.
- Размещение дочерних узлов : каждый дочерний узел размещается относительно собственного положения узла.
В конце этого этапа каждый узел макета имеет:
- Заданная ширина и высота
- Координаты x, y, где она должна быть нарисована
Вспомните дерево пользовательского интерфейса из предыдущего раздела:
Для этого дерева алгоритм работает следующим образом:
-
Row
измеряет свои дочерние элементы:Image
иColumn
. -
Image
измерено. У него нет дочерних элементов, поэтому оно само определяет свой размер и сообщает его обратно вRow
. - Далее измеряется
Column
. Сначала он измеряет своих собственных потомков (дваText
composables). - Первый
Text
измеряется. У него нет дочерних элементов, поэтому он сам определяет свой размер и сообщает его обратно вColumn
.- Второй
Text
измеряется. У него нет дочерних элементов, поэтому он сам определяет свой размер и сообщает его обратно вColumn
.
- Второй
-
Column
использует измерения дочерних элементов для определения своего собственного размера. Он использует максимальную ширину дочернего элемента и сумму высот его дочерних элементов. -
Column
размещает свои дочерние элементы относительно себя, помещая их друг под другом по вертикали. -
Row
использует измерения дочерних элементов для определения своего собственного размера. Она использует максимальную высоту дочерних элементов и сумму их ширин. Затем она размещает своих дочерних элементов.
Обратите внимание, что каждый узел был посещен только один раз. Среде выполнения Compose требуется только один проход по дереву пользовательского интерфейса для измерения и размещения всех узлов, что повышает производительность. Когда количество узлов в дереве увеличивается, время, затрачиваемое на его обход, увеличивается линейно. Напротив, если каждый узел был посещен несколько раз, время обхода увеличивается экспоненциально.
Рисунок
На этапе рисования дерево снова обходит сверху вниз, и каждый узел по очереди рисует себя на экране.
Рисунок 5. Фаза рисования рисует пиксели на экране.
Используя предыдущий пример, содержимое дерева рисуется следующим образом:
-
Row
отрисовывает любое содержимое, которое она может иметь, например, цвет фона. -
Image
рисует себя само. -
Column
рисует сама себя. - Первый и второй
Text
рисуют сами себя соответственно.
Рисунок 6. Дерево пользовательского интерфейса и его нарисованное представление.
Государство читает
Когда вы считываете value
snapshot state
во время одной из фаз, перечисленных ранее, Compose автоматически отслеживает, что он делал, когда считывал value
. Это отслеживание позволяет Compose повторно выполнять считыватель при изменении value
состояния и является основой наблюдаемости состояния в Compose.
Обычно вы создаете состояние с помощью mutableStateOf()
, а затем получаете к нему доступ одним из двух способов: напрямую обращаясь к свойству value
или, в качестве альтернативы, используя делегат свойства Kotlin. Вы можете прочитать больше о них в разделе Состояние в composables . В целях данного руководства «чтение состояния» относится к любому из этих эквивалентных методов доступа.
// State read without property delegate. val paddingState: MutableState<Dp> = remember { mutableStateOf(8.dp) } Text( text = "Hello", modifier = Modifier.padding(paddingState.value) )
// State read with property delegate. var padding: Dp by remember { mutableStateOf(8.dp) } Text( text = "Hello", modifier = Modifier.padding(padding) )
Под капотом делегата свойства функции "getter" и "setter" используются для доступа к value
State и его обновления. Эти функции getter и setter вызываются только тогда, когда вы ссылаетесь на свойство как на значение, а не когда оно создается, поэтому два способа, описанные ранее, эквивалентны.
Каждый блок кода, который может быть повторно выполнен при изменении состояния чтения, является областью перезапуска . Compose отслеживает изменения value
состояний и области перезапуска на разных фазах.
Фазовые показания состояния
Как упоминалось ранее, в Compose есть три основные фазы, и Compose отслеживает, какое состояние считывается в каждой из них. Это позволяет Compose уведомлять только конкретные фазы, которые должны выполнить работу для каждого затронутого элемента вашего пользовательского интерфейса.
В следующих разделах описывается каждая фаза и то, что происходит при считывании значения состояния в ней.
Фаза 1: Составление
Считывание состояний в функции @Composable
или лямбда-блоке влияет на композицию и, возможно, на последующие фазы. Когда value
состояния изменяется, рекомпозитор планирует повторные запуски всех составных функций, которые считывают value
этого состояния. Обратите внимание, что среда выполнения может решить пропустить некоторые или все составные функции, если входные данные не изменились. Подробнее см. в разделе Пропуск, если входные данные не изменились .
В зависимости от результата композиции Compose UI запускает фазы макета и рисования. Он может пропустить эти фазы, если содержимое останется прежним, а размер и макет не изменятся.
var padding by remember { mutableStateOf(8.dp) } Text( text = "Hello", // The `padding` state is read in the composition phase // when the modifier is constructed. // Changes in `padding` will invoke recomposition. modifier = Modifier.padding(padding) )
Фаза 2: Макет
Фаза макета состоит из двух шагов: измерение и размещение . Шаг измерения запускает лямбда-функцию измерения, переданную в компонуемый Layout
, метод MeasureScope.measure
интерфейса LayoutModifier
и другие. Шаг размещения запускает блок размещения функции layout
, лямбда-блок Modifier.offset { … }
и аналогичные функции.
Состояние чтения на каждом из этих шагов влияет на макет и потенциально на фазу рисования. Когда value
состояния изменяется, Compose UI планирует фазу макета. Он также запускает фазу рисования, если размер или положение изменились.
var offsetX by remember { mutableStateOf(8.dp) } Text( text = "Hello", modifier = Modifier.offset { // The `offsetX` state is read in the placement step // of the layout phase when the offset is calculated. // Changes in `offsetX` restart the layout. IntOffset(offsetX.roundToPx(), 0) } )
Фаза 3: Рисование
Состояние чтения во время кода рисования влияет на фазу рисования. Распространенные примеры включают Canvas()
, Modifier.drawBehind
и Modifier.drawWithContent
. Когда value
состояния изменяется, Compose UI запускает только фазу рисования.
var color by remember { mutableStateOf(Color.Red) } Canvas(modifier = modifier) { // The `color` state is read in the drawing phase // when the canvas is rendered. // Changes in `color` restart the drawing. drawRect(color) }
Оптимизация чтения состояний
Поскольку Compose выполняет отслеживание чтения локализованного состояния, вы можете минимизировать объем выполняемой работы, считывая каждое состояние в соответствующей фазе.
Рассмотрим следующий пример. В этом примере есть Image()
, который использует модификатор смещения для смещения своей окончательной позиции макета, что приводит к эффекту параллакса при прокрутке пользователем.
Box { val listState = rememberLazyListState() Image( // ... // Non-optimal implementation! Modifier.offset( with(LocalDensity.current) { // State read of firstVisibleItemScrollOffset in composition (listState.firstVisibleItemScrollOffset / 2).toDp() } ) ) LazyColumn(state = listState) { // ... } }
Этот код работает, но приводит к неоптимальной производительности. Как написано, код считывает value
состояния firstVisibleItemScrollOffset
и передает его в функцию Modifier.offset(offset: Dp)
. По мере того, как пользователь прокручивает, value
firstVisibleItemScrollOffset
будет меняться. Как вы узнали, Compose отслеживает любые чтения состояния, чтобы иметь возможность перезапустить (повторно вызвать) код чтения, который в этом примере является содержимым Box
.
Это пример чтения состояния в фазе композиции . Это не обязательно плохо, и на самом деле является основой рекомпозиции, позволяя изменениям данных генерировать новый UI.
Ключевой момент: этот пример неоптимален, поскольку каждое событие прокрутки приводит к повторной оценке, измерению, компоновке и, наконец, отрисовке всего компонуемого контента. Вы запускаете фазу Compose при каждой прокрутке, даже если отображаемый контент не изменился, изменилось только его положение . Вы можете оптимизировать чтение состояния, чтобы повторно запустить только фазу макета.
Смещение с лямбда
Доступна еще одна версия модификатора смещения: Modifier.offset(offset: Density.() -> IntOffset)
.
Эта версия принимает параметр лямбда, где результирующее смещение возвращается блоком лямбда. Обновите код, чтобы использовать его:
Box { val listState = rememberLazyListState() Image( // ... Modifier.offset { // State read of firstVisibleItemScrollOffset in Layout IntOffset(x = 0, y = listState.firstVisibleItemScrollOffset / 2) } ) LazyColumn(state = listState) { // ... } }
Так почему же это более производительно? Лямбда-блок, который вы предоставляете модификатору, вызывается во время фазы макета (в частности, во время шага размещения фазы макета), что означает, что состояние firstVisibleItemScrollOffset
больше не считывается во время композиции. Поскольку Compose отслеживает, когда состояние считывается, это изменение означает, что если value
firstVisibleItemScrollOffset
изменяется, Compose должен только перезапустить фазы макета и рисования.
Конечно, часто абсолютно необходимо читать состояния в фазе композиции. Тем не менее, есть случаи, когда вы можете минимизировать количество повторных композиций, фильтруя изменения состояний. Для получения дополнительной информации об этом см. derivedStateOf
: преобразование одного или нескольких объектов состояния в другое состояние .
Рекомпозиционный цикл (циклическая фазовая зависимость)
В этом руководстве ранее упоминалось, что фазы Compose всегда вызываются в одном и том же порядке, и что нет возможности вернуться назад, находясь в том же кадре. Однако это не запрещает приложениям попадать в циклы композиции в разных кадрах. Рассмотрим этот пример:
Box { var imageHeightPx by remember { mutableStateOf(0) } Image( painter = painterResource(R.drawable.rectangle), contentDescription = "I'm above the text", modifier = Modifier .fillMaxWidth() .onSizeChanged { size -> // Don't do this imageHeightPx = size.height } ) Text( text = "I'm below the image", modifier = Modifier.padding( top = with(LocalDensity.current) { imageHeightPx.toDp() } ) ) }
В этом примере реализован вертикальный столбец с изображением наверху и текстом под ним. Он использует Modifier.onSizeChanged()
для получения разрешенного размера изображения, а затем использует Modifier.padding()
для текста, чтобы сместить его вниз. Неестественное преобразование из Px
обратно в Dp
уже указывает на то, что в коде есть некоторые проблемы.
Проблема этого примера в том, что код не достигает "финальной" компоновки в одном кадре. Код полагается на выполнение нескольких кадров, что выполняет ненужную работу и приводит к тому, что пользовательский интерфейс прыгает по экрану для пользователя.
Композиция первого кадра
Во время фазы композиции первого кадра imageHeightPx
изначально равен 0
. Следовательно, код предоставляет тексту Modifier.padding(top = 0)
. Последующая фаза макета вызывает обратный вызов модификатора onSizeChanged
, который обновляет imageHeightPx
до фактической высоты изображения. Затем Compose планирует перекомпозицию для следующего кадра. Однако во время текущей фазы рисования текст отображается с отступом 0
, поскольку обновленное значение imageHeightPx
еще не отражено.
Композиция второго кадра
Compose инициирует второй кадр, вызванный изменением значения imageHeightPx
. В фазе композиции этого кадра состояние считывается в блоке содержимого Box
. Теперь текст снабжен отступом, который точно соответствует высоте изображения. Во время фазы макета imageHeightPx
устанавливается снова; однако дальнейшая перекомпозиция не планируется, поскольку значение остается неизменным.
Этот пример может показаться надуманным, но будьте осторожны с этой общей схемой:
-
Modifier.onSizeChanged()
,onGloballyPositioned()
или некоторые другие операции макета - Обновить некоторые состояния
- Используйте это состояние в качестве входных данных для модификатора макета (
padding()
,height()
или аналогичного) - Потенциально повторить
Исправление для предыдущего примера заключается в использовании правильных примитивов макета. Предыдущий пример можно реализовать с помощью Column()
, но у вас может быть более сложный пример, требующий чего-то пользовательского, что потребует написания пользовательского макета. См. руководство по пользовательским макетам для получения дополнительной информации
Общий принцип здесь заключается в том, чтобы иметь единый источник истины для нескольких элементов пользовательского интерфейса, которые должны быть измерены и размещены относительно друг друга. Использование надлежащего примитива макета или создание пользовательского макета означает, что минимальный общий родитель служит источником истины, который может координировать связь между несколькими элементами. Введение динамического состояния нарушает этот принцип.
{% дословно %}Рекомендовано для вас
- Примечание: текст ссылки отображается, когда JavaScript отключен.
- State и Jetpack Compose
- Списки и сетки
- Kotlin для Jetpack Compose