Compose выполняет кадр в три строго упорядоченных, последовательных этапа:

- Композиция : Запускает функции
@Composableдля построения и обновления дерева пользовательского интерфейса. - Планировка : Измеряет детей, а затем рассаживает их.
- Рисование : Выдает команды рисования на холсте для отображения пикселей на экране.
При каждом считывании State Compose на любом этапе, Compose автоматически записывает зависимость между этим состоянием и соответствующим этапом.
Что заставляет операцию записи выполняться «в обратном порядке»?
Обратная запись происходит всякий раз, когда состояние изменяется на более поздней фазе (или в нижестоящей области видимости — то есть в компонуемой области видимости, выполняемой позже в рамках одного и того же прохода композиции) , чем в той, где оно было прочитано , что вынуждает Compose выполнить повторную композицию, запланировав повторное выполнение более ранней фазы или компонуемой области видимости. Обратная запись — это неоптимизированный цикл повторной композиции.

Последствия обратной записи
Обратная запись не обязательно является чем-то плохим и не всегда приводит к сбоям, но она неэффективна и может ухудшить производительность приложения несколькими способами:
- Дополнительная отрисовка кадров и пропущенные кадры : обратная запись заставляет Compose выполнять избыточные проходы композиции по последовательным кадрам, что приводит к нерациональному использованию ресурсов ЦП и ГП и потенциально может вызвать рывки.
- Проблемы с корректностью первого кадра : если компоненту требуется обратная запись для определения его окончательных размеров или состояния, первый кадр отображается с недопустимыми, значениями по умолчанию или неустановленными данными (например, нулевым размером или неправильным смещением). Это приводит к видимым визуальным искажениям или мерцанию макета при отображении второго кадра.
- Бесконечные циклы перекомпоновки : если изменение состояния влияет на размер макета, и размер макета постоянно записывает новое значение обратно в состояние, вы рискуете создать бесконечный цикл кадров, когда экран постоянно перекомпоновывается в каждом кадре, так и не стабилизируясь.
Прямой поток через фазы
Изменения состояния всегда должны последовательно проходить через следующие фазы:
| Фаза чтения | Написать контекст | Приемлемый? | Почему |
|---|---|---|---|
Макет ( Modifier.offset { } ) | Композиция | Да | Композиция обновляет состояние → Макет считывает его позже в том же кадре без повторной композиции. |
Draw ( graphicsLayer { } , drawBehind { } ) | Композиция | Да | В фазе композиции состояние обновляется, а в фазе отрисовки оно считывается. Фазы композиции и компоновки полностью пропускаются. |
| Рисовать | Макет | Да | Макет обновляет состояние, а отрисовка считывает его, что соответствует логике работы. |
| Композиция | Обратный вызов события ( onClick , onValueChange ), управляющий изменением состояния. Примечание : обратные вызовы макета не считаются событиями. | Да | Событие изменяет состояние, используемое для управления композицией. Если событие происходит вне кадра (не в режиме композиции, компоновки или отрисовки), это считается допустимым. |
| Композиция | Корутина ( LaunchedEffect ) | Да, но с осторожностью. | Асинхронное обновление состояния происходит в ответ на события жизненного цикла. Запись данных из эффектов может быть допустимой, но может указывать на неэффективное многоуровневое управление состоянием. По возможности их следует избегать. |
| Размещение (в макете) | Размер (в макете) | Да | В методе Layout допустимо обновлять состояние, а затем считывать это состояние в методе Placement. |
| Размер (в макете) | Размещение (в макете) | Нет - обратная запись | Запись в состояние, находящееся в позиции размещения, которая затем переходит в состояние чтения, вызывает цикл переизмерения. |
| Композиция | Макет ( onSizeChanged , LayoutModifier ) | Нет — обратная запись | Изменение макета делает недействительным цикл «Композиция → Рекомпозиция». |
| Композиция | Нарисовать ( drawWithContent , Canvas ) | Нет — обратная запись | Рисование делает недействительным цикл Композиция → Перекомпозиция. |
Комбинации фаз: Обратный и прямой ход
Ниже приведены примеры обратной записи в Compose и способы их решения.
Обратный путь: чтение в процессе сочинения, написание текста в процессе верстки.
- Что происходит : Composition считывает
componentHeight, чтобы определить, какой элемент пользовательского интерфейса нужно отобразить. Позже, на этапе Layout, измеряется или размещаются представления, и вcomponentHeightзаписывается новое значение (например, с помощьюonSizeChanged,onGloballyPositionedили пользовательскогоLayoutModifier). - Результат : Изменение
componentHeightв Layout делает недействительным только что завершенный этап композиции. Обратите внимание, чтоonSizeChangedсообщает о размере после завершения этапа измерения компоновки. Если обновленное значение состояния стабилизируется на следующем этапе, перекомпозиция может остановиться через один дополнительный кадр ; однако, если новое значение продолжает изменять размер, это приводит к бесконечному циклу кадров. Кроме того,onGloballyPositionedвыполняется после компоновки и размещения, что делает запись состояния внутри него еще более подверженной непрерывным циклам перекомпозиции и перестановки кадров.
// ❌ BAD: Read in Composition, Written in Layout (onSizeChanged) @Composable fun BadAspectRatioImage(painter: Painter) { var calculatedHeight by remember { mutableStateOf(0.dp) } val density = LocalDensity.current // State read during COMPOSITION: Image( painter = painter, contentDescription = "Dynamic Image", modifier = Modifier .fillMaxWidth() .height(calculatedHeight) .onSizeChanged { size -> // State write during LAYOUT phase! // Triggers backwards write and recomposition pass val aspectRatio = 16f / 9f val widthDp = with(density) { size.width.toDp() } calculatedHeight = widthDp / aspectRatio } ) } // ✅ GOOD: Measure and calculate aspect ratio height in Phase 2 (Layout) without recomposition @Composable fun GoodAspectRatioImage( painter: Painter, aspectRatio: Float = 16f / 9f, modifier: Modifier = Modifier ) { Layout( content = { Image( painter = painter, contentDescription = "Dynamic Image" ) }, modifier = modifier ) { measurables, constraints -> val width = constraints.maxWidth val height = (width / aspectRatio).toInt() // Illustrative, you can use Modifier.aspectRatio() val imageConstraints = constraints.copy( minWidth = width, maxWidth = width, minHeight = height, maxHeight = height ) val placeable = measurables.first().measure(imageConstraints) layout(width, height) { placeable.placeRelative(0, 0) } } }
Обратный путь: чтение в процессе сочинения, письмо в процессе рисования.
- Что происходит : состояние считывается в теле объекта Composable (фаза композиции), но изменяется внутри
Modifier.drawWithContent,Modifier.drawBehindилиCanvas(фаза отрисовки). - Результат : Фаза отрисовки меняет состояние → Композиция аннулирована → бесконечный цикл.
// ❌ BAD: Read in Composition, Written in Draw () @Composable fun BadBackwardsWriteDraw() { var componentHeight by remember { mutableStateOf(0.dp) } // State read during COMPOSITION: Text( text = "Height is: $componentHeight", modifier = Modifier.drawBehind { // State write during the DRAW phase! // Invalidates Composition -> triggers recomposition loop! componentHeight = size.height.dp } ) }
Обратный порядок: чтение в процессе сочинения, письмо в процессе сочинения (один и тот же этап).
- Что происходит : чтение
countв компонуемой функции иcountнепосредственное изменение в другом слоте содержимого компонуемого объекта после чтения. - Результат : Система моментальных снимков записывает операции чтения и последующей записи в рамках одного прохода композиции, немедленно аннулируя текущую область видимости.
// ❌ BAD: Direct write in Composable body after read @Composable fun BadCounter() { var count by remember { mutableIntStateOf(0) } Text("Count: $count") // State read in Composition Button(onClick = {}) { count++ // State write in Composition (Backwards write!) } } // Acceptable - but error-prone as someone may add a read before the write : Direct write in Composable body before read @Composable fun OkCounter() { var count by remember { mutableIntStateOf(0) } Button(onClick = {}) { count++ // State write in Composition } Text("Count: $count") // State read in Composition }
Основные правила предотвращения обратной записи
- Не следует записывать данные в состояние в
onGloballyPositioned,onSizeChangedилиLayoutModifierесли это состояние считывается в Composition, поскольку это вызывает проблему корректности первого кадра.- Если координаты или размеры макета необходимы только для пользовательской отрисовки, считывайте их непосредственно на этапе отрисовки или компоновки (например, используя
Modifier.drawWithCacheилиModifier.layout). - Для определения размера на уровне окна (
WindowWidthSizeClass): Поднимите наблюдение за размером на уровень окна. Композиция разветвляется по классам размеров окна до того, как произойдет локальное измерение. - Сохраняйте единообразие композиции: используйте единый пользовательский макет или такие компоненты, как
FlowRowилиLazyVerticalGrid, которые корректируют размеры и размещение на этапе 2 без перекомпоновки или изменения состояния, использованного при композиции. - Используйте подкомпозицию: используйте
BoxWithConstraintsилиSubcomposeLayoutкогда дочерние компонуемые элементы должны разветвляться в зависимости от локальной ширины или высоты. Будьте осторожны: подкомпозиция влечет за собой снижение производительности и обычно ее можно избежать. - В крайнем случае: допустите ошибку в первом кадре, сохранив размер в
onSizeChanged, чтобы запустить перекомпоновку второго кадра. Это приведет к видимым искажениям макета, рывкам и риску бесконечных циклов.
- Если координаты или размеры макета необходимы только для пользовательской отрисовки, считывайте их непосредственно на этапе отрисовки или компоновки (например, используя
- Не изменяйте состояние после его первого прочтения в композиции :
- Хотя вы можете безопасно записывать данные в объекты
MutableStateво время композиции внеSideEffect, следует проявлять особую осторожность, чтобы не записывать данные в состояние, которое вы могли ранее прочитать во время композиции. Рекомендуется использоватьrememberUpdatedStateкогда возникает необходимость записи в состояние во время композиции. Запись в состояние во время композиции другим способом обычно свидетельствует об отсутствии эффекта или неправильно спроектированном состоянии или компонуемом объекте. Помните, что композиция является оптимистичной и всегда выполняется с последним значением состояния, поэтому вы можете не увидеть все изменения состояния при перекомпозициях. Обновления пользовательского интерфейса не следует использовать для обработки разовых событий, поэтому редко требуется обновление значения состояния в результате перекомпозиции. - Избегайте изменения состояний, наблюдаемых вне композиции (например, полей
ViewModelили флаговisVisible). Запись состояния, влияющая на композицию, должна осуществляться в лямбда-функциях событий (onClick), сопрограммах (LaunchedEffect) или побочных эффектах (SideEffect).rememberUpdatedStateявляется исключением, поскольку она предназначена для изменения состояния, используемого только в теле аннотации@Composable.
- Хотя вы можете безопасно записывать данные в объекты
- Отложить чтение состояния до максимально поздней фазы :
- Чтение состояний в Draw (
Modifier.graphicsLayer { alpha = ... }) или Layout (Modifier.offset { IntOffset(...) }) гарантирует, что изменения будут аннулировать только фазу 2 или 3, полностью пропуская фазу 1 (Композиция).
- Чтение состояний в Draw (