Разработка
Экспериментальный Styles API в Jetpack Compose
Экспериментальный Styles API представляет новую парадигму для определения интерактивных, stateful элементов пользовательского интерфейса в Jetpack Compose.
Система модификаторов Jetpack Compose долгое время была основным способом применения визуальных свойств к компонуемым элементам. Вы объединяете модификаторы, такие как background(), padding() и border(), для здания внешнего вида и поведения элементов пользовательского интерфейса. Несмотря на свою мощь, этот подход имеет ограничения при работе с интерактивными состояниями. Если вы хотите, чтобы кнопка меняла цвет при нажатии, вам необходимо вручную отслеживать состояние, создавать анимированные значения и условно применять различные модификаторы. Новый экспериментальный Styles API призван решить эту проблему, предоставляя декларативный способ определения зависимых от состояния стилей с автоматической анимацией.
В этой статье вы изучите, как работает Styles API, рассмотрев, как объекты Style инкапсулируют визуальные свойства в виде компонуемых лямбда-выражений, как StyleScope предоставляет доступ к свойствам макета, рисования и текста, как StyleState предоставляет доступ к состояниям взаимодействия, таким как нажатый, наведенный и сфокусированный, как система автоматически анимирует переходы между состояниями стиля без ручного управления Animatable, и как двухузловая архитектура модификаторов эффективно применяет стили, минимизируя при этом ненужные действия. Это не руководство по базовой стилизации в Compose — это исследование новой парадигмы определения внешнего вида интерактивных, stateful UI-компонентов.
Проблема со stateful стилизацией
Рассмотрите возможность реализации кнопки, которая меняет цвет при наведении курсора и нажатии. При текущем подходе с использованием модификатора вам придется управлять этим вручную:
@Composable
fun InteractiveButton(onClick: () -> Unit) {
val interactionSource = remember { MutableInteractionSource() }
val isPressed by interactionSource.collectIsPressedAsState()
val isHovered by interactionSource.collectIsHoveredAsState()
val backgroundColor by animateColorAsState(
targetValue = when {
isPressed -> Color.Red
isHovered -> Color.Yellow
else -> Color.Green
}
)
Box(
modifier = Modifier
.clickable(interactionSource = interactionSource, indication = null) { onClick() }
.background(backgroundColor)
.size(150.dp)
)
Этот шаблон требует нескольких компонентов: InteractionSource для отслеживания взаимодействий, state derivation для каждого типа взаимодействия, анимированных значений для плавных переходов и условной логики для определения текущего внешнего вида. Код многословен, а задачи распределены по нескольким объявлениям.
Styles API объединяет всё это в единое декларативное определение:
@Composable
fun InteractiveButton(onClick: () -> Unit) {
ClickableStyleableBox(
onClick = onClick,
style = {
background(Color.Green)
size(150.dp)
hovered { animate { background(Color.Yellow) } }
pressed { animate { background(Color.Red) } }
}
)
}
Блок стиля определяет как внешний вид по умолчанию, так и то, как он изменяется в разных состояниях. Обертка animate указывает системе плавно длеать переход при входе или выходе из этого состояния. Никакого ручного отслеживания состояния, никаких явных анимированных значений, никаких цепочек условных модификаторов.
Стиль: функциональный интерфейс для визуальных свойств
Интерфейс Style является основой API. Он определяется как функциональный интерфейс, работающий с StyleScope:
@ExperimentalFoundationStyleApi
public fun interface Style {
public fun StyleScope.applyStyle()
}
Такая конструкция позволяет создавать стили с использованием синтаксиса лямбда-выражений. Когда вы пишете style = { background(Color.Green) }, вы создаёте экземпляр класса Style, функция applyStyle которого вызывает background(Color.Green) в принимающем StyleScope.
Стили можно комбинировать с помощью инфиксной функции then или фабричных функций Style():
val baseStyle = Style {
background(Color.White)
contentPadding(16.dp)
}
val borderedStyle = Style {
borderWidth(1.dp)
borderColor(Color.Gray)
}
val combinedStyle = baseStyle then borderedStyle
При комбинировании стилей свойства более поздних стилей переопределяют свойства более ранних стилей для каждого свойства отдельно. Это отличается от цепочки модификаторов, где применяются оба модификатора, а порядок определяет визуальное наложение. В случае со стилями, если и baseStyle, и borderedStyle задают background, используется только второе значение.
Комбинирование реализуется с помощью CombinedStyle, внутреннего класса, который содержит массив стилей и применяет их последовательно:
internal class CombinedStyle(val styles: Array<Style>) : Style {
override fun StyleScope.applyStyle() {
styles.fastForEach { with(it) { applyStyle() } }
}
}
StyleScope: поверхность свойств
StyleScope — это sealed интерфейс, предоставляющий все свойства, которые можно задать в стиле. Он расширяет CompositionLocalAccessorScope, предоставляя стилям доступ к значениям темы, и Density, позволяя преобразовывать значения из dp в пиксели:
@ExperimentalFoundationStyleApi public sealed interface StyleScope : CompositionLocalAccessorScope, Density { public val state: StyleState // ... property functions }
Свойство state имеет решающее значение, поскольку обеспечивает доступ к текущему состоянию взаимодействия, позволяя применять условное оформление в зависимости от того, нажат ли элемент, находится ли он под курсором, есть ли на нем фокус или он находится в других состояниях.
Скоуп предоставляет набор функций, сгруппированных по нескольким категориям.
Layout-свойства отвечают за размеры и отступы: width(), height(), size() — для явного задания размеров; fillWidth(), fillHeight(), fillSize() — для заполнения доступного пространства (в том числе с долей от родителя); minWidth(), maxWidth(), minHeight(), maxHeight() — для задания ограничений; contentPadding() и externalPadding() — для внутренних и внешних отступов соответственно.
Drawing-свойства управляют визуальным оформлением: background(Color) и background(Brush) — заливка; borderWidth(), borderColor() и border() — параметры обводки; shape() — скругление углов и кастомные формы; dropShadow() и innerShadow() — эффекты тени и псевдо-«elevation».
Transform-свойства отвечают за пространственные трансформации: alpha() — прозрачность; scaleX(), scaleY(), scale() — масштабирование;translationX(), translationY(), translation() — смещение по осям; rotationX(), rotationY(), rotationZ() — 3D-повороты; clip() и zIndex() — клиппирование и управление порядком наложения.
Text-свойства управляют типографикой внутри стилизуемого элемента: textStyle() — комплексная настройка текста; fontSize(), fontWeight(), fontFamily() — параметры шрифта; contentColor() и contentBrush() — цвет или кисть для текста; letterSpacing(), lineHeight(), textAlign() — параметры текстового лейаута.
StyleState: знакомый с взаимодействиями
Интерфейс StyleState отображает текущее состояние взаимодействия стилизованного элемента:
public sealed interface StyleState {
public val isEnabled: Boolean
public val isFocused: Boolean
public val isHovered: Boolean
public val isPressed: Boolean
public val isSelected: Boolean
public val isChecked: Boolean
public val triStateToggle: ToggleableState
public operator fun <T> get(key: StyleStateKey<T>): T
}
Эти свойства считываются во время разрешения стиля. При написании условной логики на основе state.isPressed система стилей отслеживает эту зависимость и повторно разрешает стиль при изменении состояния нажатия.
StyleScope предоставляет удобные функции для распространенных шаблонов состояний:
style = {
background(Color.Green)
hovered {
background(Color.Yellow)
}
pressed {
background(Color.Red)
}
focused {
borderWidth(2.dp)
borderColor(Color.Blue)
}
}
Это синтаксический сахар для шаблонов if (state.isHovered) { ... }, но они также позволяют системе анимации понимать переходы состояний.
Автоматические анимации
Одной из самых мощных функций Styles API является декларативная анимация. Вместо того чтобы вручную создавать экземпляры Animatable и запускать корутины, вы оборачиваете изменения стиля в animate:
style = {
background(Color.Blue)
size(150.dp)
hovered {
animate {
background(Color.Yellow)
scale(1.1f)
}
}
pressed {
animate(tween(100)) {
background(Color.Red)
scale(0.95f)
}
}
}
Функция animate принимает необязательные параметры AnimationSpec для настройки перехода. Когда элемент переходит в состояние наведения курсора, система автоматически анимирует переход от текущего цвета фона к жёлтому и от текущего масштаба к 1.1f. Когда элемент выходит из состояния наведения курсора, анимация возвращается к исходному состоянию.
Система анимации управляется внутренним классом StyleAnimations, который отслеживает активные анимации:
internal class StyleAnimations {
private val entries = mutableObjectListOf<Entry>()
private class Entry(
val key: Any,
var style: ResolvedStyle,
val toSpec: AnimationSpec<Float>,
val fromSpec: AnimationSpec<Float>,
val animatable: Animatable<Float, AnimationVector1D>,
var state: State,
)
}
Каждый блок анимированного стиля получает объект Entry, отслеживающий текущий прогресс анимации. Функция withAnimations применяет интерполированные значения к разрешенному стилю с использованием линейной интерполяции на основе текущего значения каждой анимации.
Система автоматически обрабатывает несколько сложных моментов: одновременные анимации при перекрытии нескольких изменений состояния, прерывание при изменении состояния в середине анимации, а также анимации входа/выхода при добавлении или удалении стилей из композиции.
ResolvedStyle: представление в рантайме
При применении стиля он преобразуется в экземпляр ResolvedStyle, который содержит конкретные значения для всех свойств:
internal class ResolvedStyle : StyleScope, InspectableValue {
// Layout properties
var contentPaddingStart: Dp = Dp.Unspecified
var contentPaddingEnd: Dp = Dp.Unspecified
var width: Dp = Dp.Unspecified
var height: Dp = Dp.Unspecified
// ... approximately 50 properties
// Optimization flags
private var layoutFlags: Int = 0
private var drawFlags: Int = 0
private var textFlags: Int = 0
}
В этом классе используется система маркировки на основе битовых наборов для отслеживания того, какие свойства были установлены. Эта оптимизация служит двум целям: она позволяет системе различать состояния «не установлено» и «установлено значение по умолчанию», а также обеспечивает эффективное обнаружение изменений путем сравнения целых чисел флагов, а не отдельных свойств.
Значения перечислений, относящиеся к тексту, упаковываются в одно целочисленное поле с помощью побитового сдвига:
private var textEnums: Int = 0 // Packed: fontWeight | fontStyle | fontSynthesis | textDecoration | // textAlign | textDirection | hyphens | lineBreak
Это уменьшает объем памяти, занимаемый каждым экземпляром ResolvedStyle, при сохранении быстрого доступа к отдельным значениям за счет операций битового маскирования.
Двухузловая архитектура модификаторов
Стили применяются к элементам через расширение модификатора styleable:
public fun Modifier.styleable(
styleState: StyleState?,
style: Style,
): Modifier
Реализация использует два Modifier-узла (ноды): внешний (outer) и внутренний (inner). Как отмечается в коде: «Требуются два LayoutModifierNode. Внешний модификатор реализует почти всё, кроме padding. Чтобы padding, отрисовка и прочее работали корректно, необходим внутренний модификатор, который добавляет отступы».
StyleOuterNode отвечает за обработку layout-ограничений (Constraints), измерение (measure), отрисовку фона, применение трансформаций, тени, большую часть стилевых модификаций. StyleInnerNode занимается исключительно content padding.
Важно, что padding должен применяться после внешних модификаций, чтобы поведение layout было корректным (особенно при комбинации с трансформациями, клиппингом и кастомной отрисовкой).
Внешний узел реализует несколько узловых интерфейсов:
internal class StyleOuterNode :
LayoutModifierNode,
DrawModifierNode,
CompositionLocalConsumerModifierNode,
ObserverModifierNode,
TraversableNode
Эта реализация с несколькими интерфейсами позволяет одному узлу участвовать в компоновке, отрисовке, локальном наблюдении за композицией и обходе дерева, минимизируя количество узлов в цепочке модификаторов.
Выборочная аннулирование
Система стилей тщательно отслеживает, какие подсистемы нуждаются в инвалидации при изменении свойств. ResolvedStyle поддерживает отдельные флаги для изменений компоновки, отрисовки и текста:
internal fun invalidate(previous: ResolvedStyle): Int {
var result = 0
if (layoutChanged(previous)) result = result or LAYOUT_INVALIDATION
if (drawChanged(previous)) result = result or DRAW_INVALIDATION
if (textChanged(previous)) result = result or TEXT_INVALIDATION
return result
}
Когда стиль изменяет только свойства отрисовки, такие как фон или альфа-канал, аннулируется только фаза отрисовки. Компоновка и композиция остаются неизменными. Эта детальная инвалидация аналогична работе системы фаз Compose, где изменения свойств graphicsLayer запускают только отрисовку без перекомпозиции или перестановки.
Система анимации также возвращает флаги аннулирования после применения анимированных значений, гарантируя, что в каждом кадре анимации выполняются только необходимые фазы.
Доступ к локальным элементам композиции
Поскольку StyleScope расширяет CompositionLocalAccessorScope, стили могут напрямую считывать локальные элементы композиции:
style = {
val colors = LocalColors.current
background(colors.surface)
contentColor(colors.onSurface)
pressed {
background(colors.surfaceVariant)
}
}
Эта интеграция означает, что стили могут учитывать тему без необходимости явного указания параметров цвета. При изменении темы стили, считывающие значения темы, автоматически переопределяются.
Отслеживание осуществляется узлом ObserverModifierNode, который отслеживает локальные значения, считываемые при разрешении стиля, и аннулирует узел при изменении этих значений.
Подводя итог
Styles API представляет собой новые API для того, чтобы Compose обрабатывал интерактивное стилирование. Объединяя отслеживание состояния, автоматическую анимацию и выборочную аннулирование в единый декларативный API, он уменьшает количество шаблонного кода, сохраняя при этом производительность.
Полный пример демонстрирует выразительность API:
@Composable
fun StyledCard(
title: String,
onClick: () -> Unit,
modifier: Modifier = Modifier
) {
val cardStyle = Style {
background(MaterialTheme.colorScheme.surface)
shape(RoundedCornerShape(12.dp))
contentPadding(16.dp)
dropShadow(4.dp, Color.Black.copy(alpha = 0.1f))
hovered {
animate(tween(200)) {
dropShadow(8.dp, Color.Black.copy(alpha = 0.15f))
translationY((-2).dp)
}
}
pressed {
animate(tween(100)) {
dropShadow(2.dp, Color.Black.copy(alpha = 0.05f))
scale(0.98f)
}
}
focused {
borderWidth(2.dp)
borderColor(MaterialTheme.colorScheme.primary)
}
}
ClickableStyleableBox(
onClick = onClick,
modifier = modifier,
style = cardStyle
) {
Text(title)
}
}
Это единое определение стиля обрабатывает внешний вид по умолчанию, эффекты при наведении курсора с изменением тени и положения, эффекты нажатия с анимацией масштабирования и индикацию фокуса с рамкой. Все переходы автоматические, отслеживание состояния неявное, и система обеспечивает минимальную недействительность при обновлениях.
Заключение
Экспериментальный Styles API представляет новую парадигму для определения интерактивных, stateful элементов пользовательского интерфейса в Jetpack Compose. Вместо ручной настройки InteractionSource, определения состояний и анимированных значений в разрозненных объявлениях, API объединяет логику стилизации в целостные, декларативные блоки.
Функциональный интерфейс Style инкапсулирует визуальные свойства в виде компонуемых лямбда-выражений, работающих в StyleScope. Эта область видимости обеспечивает полный доступ к свойствам макета, таким как размеры и отступы, свойствам отрисовки, таким как фон и границы, свойствам преобразования, таким как масштаб и вращение, и текстовым свойствам для управления типографикой. Интерфейс StyleState предоставляет доступ к состояниям взаимодействия, включая нажатый, наведенный, сфокусированный, выбранный и отмеченный, что позволяет применять условное стилирование без явного управления состоянием.
Система анимации обрабатывает переходы автоматически. Оборачивание изменений стиля в блоки анимации указывает системе на необходимость плавной интерполяции между состояниями, с возможностью настройки параметров AnimationSpec. Класс StyleAnimations управляет отслеживанием входа, интерполяцией и обработкой параллельной анимации внутри.
Внутри системы двухузловая архитектура модификаторов разделяет внешние изменения (компоновка, отрисовка, преобразования) от внутренних изменений (отступы контента) для обеспечения корректного поведения. Класс ResolvedStyle хранит около 50 свойств с оптимизацией на основе битовых наборов для повышения эффективности использования памяти и обнаружения изменений. Выборочная инвалидация гарантирует, что изменения свойств отрисовки запускают только фазу отрисовки, в то время как изменения компоновки запускают компоновку и отрисовку, но пропускают композицию.
Styles API в настоящее время является экспериментальным и может быть изменен, что отмечено аннотацией @ExperimentalFoundationStyleApi. Однако он демонстрирует перспективное направление для Compose: объединение интерактивной стилизации в декларативные определения, которые фреймворк может автоматически оптимизировать. По мере развития API он может коренным образом изменить подход к стилизации пользовательского интерфейса с сохранением состояния в приложениях Compose.
Как всегда, удачного программирования!
-
Маркетинг и монетизация3 недели назад
Как ML-подход удвоил первые покупки при снижении CPI, CAC, ДРР: «Яндекс Маркет» и Bidease
-
Новости4 недели назад
Видео и подкасты о мобильной разработке 2026.6
-
Видео и подкасты для разработчиков2 недели назад
КодРевью лидера мнений: как можно нарушить сразу все принципы разработки
-
Новости3 недели назад
Видео и подкасты о мобильной разработке 2026.7
