1. Введение
Всем привет!
В этой статье мы будем обсуждать, как писать масштабируемые и поддерживаемые интерфейсы на Jetpack Compose: от базовых компонентов до архитектурных практик. Разбираем иерархию, принципы проектирования, naming, порядок параметров и антипаттерны.
Думаю, что статья будет полезна как новичкам, так и опытным разработчикам.
Jetpack Compose — современный инструмент от Google для создания нативных интерфейсов Android, который упрощает и ускоряет разработку UI с использованием декларативного подхода.
Хотя Compose уже давно вышел в релиз и плотно вошел в повседневную разработку, многие элементы даже сейчас требуют аннотации @Experimental
и впоследствии могут измениться.
В этой статье я поделюсь актуальными рекомендациями и личным опытом, которые помогут вам создавать масштабируемые и поддерживаемые интерфейсы.
1.1 От простого к сложному: построение иерархии компонентов
В Jetpack Compose UI строится из функций-компонентов, которые вызывают другие функции компонентов, их можно условно разделить на два уровня:
Нижний уровень (Low-level) - базовые элементы интерфейса, такие как
Button
,Text
,Row
,Column
,Box
, предоставляемые библиотекой Material Design 3.Верхний уровень (High-level) - кастомные компоненты, созданные на основе базовых элементов нижнего уровня, которые инкапсулируют определенную функциональность и могут быть переиспользованы в различных частях приложения.
Грамотно построенная иерархия компонентов способствует улучшению читаемости кода, упрощает тестирование и облегчает командную работу. Это приобретает особую значимость при разработке сложных интерфейсов в составе крупных команд.
Также рекомендую ознакомиться со всеми компонентами из гайдлайнов Material Design. Это поможет лучше понимать назначение каждого элемента и быстрее строить осмысленные и гибкие компоненты высокого уровня. Ссылка на сайт
Глубокое понимание API и технических ограничений позволяет разработчику не просто реализовывать требования, но и предлагать более рациональные решения. Иногда именно инженер замечает, что задумка дизайнера или продукт‑менеджера приводит к излишне сложной реализации, и может предложить альтернативу, которая упростит разработку, снизит затраты и улучшит пользовательский опыт.
1.2 Принцип единственной ответственности в Compose-компонентах
Компания Google отмечает, что при проектировании в Compose рекомендуется придерживаться принципа единственной ответственности (Single Responsibility Principle). Каждый компонент должен выполнять одну конкретную функцию. Это способствует улучшению читаемости, тестируемости и переиспользуемости компонентов.
Например, вместо создания одного большого компонента, который обрабатывает множество задач, лучше разделить его на несколько мелких, каждый из которых отвечает за свою часть функциональности. Такой подход упрощает тестирование и поддержку кода, делает структуру интерфейса более прозрачной.
Пример кода
//Композиция мелких компонентов
@Composable
fun UserProfile(
avatarUrl: String,
userName: String,
userStatus: String,
isFollowing: Boolean,
onFollowClick: () -> Unit,
modifier: Modifier = Modifier
) {
Row(
modifier = modifier.padding(16.dp),
verticalAlignment = Alignment.CenterVertically
) {
Avatar(imageUrl = avatarUrl)
Spacer(Modifier.width(16.dp))
UserInfo(name = userName, status = userStatus)
Spacer(Modifier.weight(1f))
FollowButton(isFollowing = isFollowing, onClick = onFollowClick)
}
}
//Загрузка и отображение аватарки
@Composable
fun Avatar(
imageUrl: String,
modifier: Modifier = Modifier
) {
AsyncImage(
model = imageUrl,
contentDescription = "User Avatar",
modifier = modifier.size(64.dp)
)
}
//Отображение имени и статуса
@Composable
fun UserInfo(
name: String,
status: String,
modifier: Modifier = Modifier
) {
Column(modifier = modifier) {
Text(text = name, style = MaterialTheme.typography.titleMedium)
Text(text = status, style = MaterialTheme.typography.bodySmall)
}
}
//Логика подписки
@Composable
fun FollowButton(
isFollowing: Boolean,
onClick: () -> Unit,
modifier: Modifier = Modifier
) {
Button(
onClick = onClick,
modifier = modifier
) {
Text(text = if (isFollowing) "Unfollow" else "Follow")
}
}
Главным сигналом для выделения отдельного компонента должна быть проблема, повторяющаяся в нескольких частях кода. Именно это — индикатор, что имеет смысл создать переиспользуемый UI-блок, с помощью которого можно решить задачу в разных местах приложения.
Если же UI является уникальным и строго привязан к конкретному экрану, стоит задуматься: возможно, не нужно выносить его в отдельный компонент.
Избыточная абстракция может усложнить код без очевидной пользы.
Хороший компонент — это не просто обёртка, а решение, которое масштабирует интерфейс и устраняет дублирование.
2. Базовые и высокоуровневые компоненты
2.1 Подход к именованию
Если вы создаете собственные компоненты, используя API нижнего уровня (например, Button
, Box
, Text
и другие базовые элементы Compose), то часто рекомендуется добавлять префикс Base
в название. Это сигнализирует другим разработчикам, что это «сырой» или минималистичный компонент, без оформления, без привязки к дизайну. Предполагается, что вы сами добавите стили, размеры, отступы и т.д. Используется, когда вы хотите полный контроль над внешним видом.
Стандартный же Component
- это готовый к использованию компонент. Он уже оформлен по определённой дизайн-системе (например, Material Design 3).
Пример кода
//Базовый компонент (`BaseButton`) — без стилей, только логика
@Composable
fun BaseButton(
onClick: () -> Unit,
modifier: Modifier = Modifier,
content: @Composable () -> Unit
) {
Button(
onClick = onClick,
modifier = modifier,
contentPadding = PaddingValues(0.dp), // Убираем стандартные отступы
colors = ButtonDefaults.buttonColors(containerColor = Color.Transparent), // Прозрачный фон
elevation = null, // Убираем тень
content = { content() }
)
}
//Готовый стилизованный компонент (`PrimaryButton`) на основе `BaseButton`
@Composable
fun PrimaryButton(
text: String,
onClick: () -> Unit,
modifier: Modifier = Modifier
) {
BaseButton(
onClick = onClick,
modifier = modifier
) {
Text(
text = text,
color = Color.White,
modifier = Modifier
.padding(horizontal = 24.dp, vertical = 12.dp)
)
}
}
Таким образом, Base-компоненты
— это строительные блоки, на основе которых создаются high-level решения, адаптированные под конкретные задачи или сценарии интерфейса.
2.2 Наименование должно отражать назначение
Важно использовать PascalCase
для названий функций и избегать префиксов get
, show
или display,
и кроме структурных соглашений, необходимо, чтобы названия компонентов описывали их цель, а не техническую реализацию.
Пример кода
//Название здорового человека
@Composable
fun UserProfileCard(
userName: String,
userAvatar: String
) {
Card {
Row {
AsyncImage(model = userAvatar, contentDescription = "Avatar")
Text(text = userName)
}
}
}
//Название курильщика
@Composable
fun ColumnWithImageAndText(
text: String,
imageUrl: String
) {
Column {
Image(painter = rememberAsyncImagePainter(imageUrl), contentDescription = null)
Text(text = text)
}
}
Это значительно упрощает понимание и навигацию в коде всей команде, поэтому придерживайтесь единого подхода в именовании компонентов в рамках проекта.
2.3 Параметры компонента
В Jetpack Compose принято придерживаться определённого порядка параметров в @Composable
функциях, чтобы обеспечить единообразие, читаемость и предсказуемость. Ниже — рекомендуемый порядок, который используется в библиотеках Jetpack и в крупных проектах:
Обязательные параметры данных
То, без чего компонент не имеет смысла: текст, изображение и т.д.Callbacks (действия)
Обработчики кликов, изменений, интеракций.Modifier
Обязательно передается последним из "содержательных" параметров, с дефолтом.Опциональные параметры управления (State)
Например, состояния загрузки, ошибок, видимости.Slot-лямбды (content, icon, label и т.п.)
Лямбды-композиции, если они есть. Обычно идут в конце.
Пример кода
@Composable
fun Button(
onClick: () -> Unit, //Обязательный параметр
modifier: Modifier = Modifier,//Modifier
enabled: Boolean = true,
shape: Shape = ButtonDefaults.shape,
colors: ButtonColors = ButtonDefaults.buttonColors(),
elevation: ButtonElevation? = ButtonDefaults.buttonElevation(), // Опциональный параметр
border: BorderStroke? = null, // Опциональный параметр
contentPadding: PaddingValues = ButtonDefaults.ContentPadding,
interactionSource: MutableInteractionSource? = null,// Опциональный параметр
content: @Composable RowScope.() -> Unit //Slot-лямбды
) {
}
2.4 Инициализация параметров
Понимание того, как инициализируются параметры в Compose, помогает избежать ненужных проверок, упростить API и сделать компонент более читаемым. Обычно параметры можно разделить на три категории:
null-параметры
пустые значения
значения по умолчанию
2.5 Null-параметры
Параметр со значением null означает, что компонент не требует его обязательно. Это сигнал, что значение может быть опущено, и компонент сам решит, как поступить — инициализировать его по умолчанию или вовсе проигнорировать.
Например, у кнопки Button
параметр border может быть null, если вы не хотите отображать рамку.
Пример кода
@Composable
fun Button(
***//Null-параметры
border: BorderStroke? = null,//
interactionSource: MutableInteractionSource? = null,
***
) {
}
2.6 Пустые-параметры
Параметры, которые могут быть пустыми, означают, что компонент ожидает их присутствие, но допускает, что они могут быть пустыми или не содержать видимого контента. Это даёт возможность использовать компонент с разным наполнением, не ломая логику.
Хороший пример — параметр content
у компонента Chip
в Jetpack Compose. Этот слот всегда должен быть передан, но может содержать пустой или минимальный UI, если нужно показать пустой или сдержанный элемент.
Такой подход помогает делать компоненты гибкими и адаптированными к разным ситуациям без лишних проверок на null.
Пример кода
@Composable
fun TopAppBar(
title: @Composable () -> Unit,
modifier: Modifier = Modifier,
navigationIcon: @Composable () -> Unit = {}, //Пустые параметры
actions: @Composable RowScope.() -> Unit = {}, //Пустые параметры
expandedHeight: Dp = TopAppBarDefaults.TopAppBarExpandedHeight,
windowInsets: WindowInsets = TopAppBarDefaults.windowInsets,
colors: TopAppBarColors = TopAppBarDefaults.topAppBarColors(),
scrollBehavior: TopAppBarScrollBehavior? = null,
) {
}
2.7 Defaults-параметры
Дефолтные параметры не должны быть null. Они обязаны быть явными, публичными и содержательными — то есть содержать конкретное значение, подходящее для большинства случаев. Особенно это актуально для параметров, связанных со стилем: цвета, размеры, отступы, шрифты и т.д.
Одна из лучших практик — вынос таких параметров в отдельный object
, например ChipDefaults
, ButtonDefaults
, TextFieldDefaults
и т.п.
Это делает стиль компонента централизованным и легко управляемым. При необходимости изменить визуальное поведение — вы просто обновляете нужное значение в одном месте, и оно применяется во всех компонентах, которые используют этот дефолт.
Пример кода
@Composable
fun Button(
onClick: () -> Unit,
modifier: Modifier = Modifier,
enabled: Boolean = true,
shape: Shape = ButtonDefaults.shape,
colors: ButtonColors = ButtonDefaults.buttonColors(),
elevation: ButtonElevation? = ButtonDefaults.buttonElevation(),
border: BorderStroke? = null,
contentPadding: PaddingValues = ButtonDefaults.ContentPadding,
interactionSource: MutableInteractionSource? = null,
content: @Composable RowScope.() -> Unit
) {
}
object ButtonDefaults {
private val ButtonLeadingSpace = BaselineButtonTokens.LeadingSpace
private val ButtonTrailingSpace = BaselineButtonTokens.TrailingSpace
private val ButtonWithIconStartpadding = 16.dp
private val SmallStartPadding = ButtonSmallTokens.LeadingSpace
private val SmallEndPadding = ButtonSmallTokens.TrailingSpace
private val ButtonVerticalPadding = 8.dp
val ContentPadding =
PaddingValues(
start = ButtonLeadingSpace,
top = ButtonVerticalPadding,
end = ButtonTrailingSpace,
bottom = ButtonVerticalPadding
)
val ButtonWithIconContentPadding =
PaddingValues(
start = ButtonWithIconStartpadding,
top = ButtonVerticalPadding,
end = ButtonTrailingSpace,
bottom = ButtonVerticalPadding
)
}
3. Важные привычки
Несколько моментов, которые стоит учитывать при проектировании компонентов в Jetpack Compose. Эти принципы помогут писать чистые, переиспользуемые и масштабируемые интерфейсы, где UI чётко отделён от логики, а компоненты легко комбинируются и настраиваются. Ниже — самые полезные практики, которые стоит превратить в привычку.
Переиспользуемость, через параметры
Открывайте компоненты для конфигурации, через параметры. Не плодите пять однотипных кнопок — сделайте одну, но с параметрами, управляющими цветом, иконкой, состоянием.Избегайте бизнес-логики внутри UI
UI-компоненты должны быть декларативными: они просто отображают то, что передано. Обработка событий, валидация, навигация — всё это лучше выносить в ViewModel или в слой управления состоянием (UiState, Intent, Reducer)Концепция state hoisting
Это шаблон организации компонентов, при котором UI-компонент не хранит состояние внутренне, а получает его извне через параметры, и передает изменения обратно через callback. Это позволяет разделить Stateless UI и Stateful-логику, делая код модульным. Подробнее тутModifier никогда не бывает лишним
Всегда передавайтеModifier
как параметр в свои компоненты. Это позволит пользователю компонента гибко настраивать отступы, размеры и анимации извне.Лишь один Modifier(“There Can Be Only One”)
Ваша функция должна принимать лишь одинModifier
, и следует помнить, что применять его стоит только к верхнему корневому контейнеру Composable, тем самым вы избегайте его использование в глубине иерархии.Соблюдайте единый стиль оформления
Используйте отступы, размеры, типографику и цвета из MaterialTheme или собственных design-tokens. Это обеспечит визуальное единство и упростит поддержку: при необходимости вы сможете изменить стиль в одном месте и он обновится во всех компонентах.Пишите превью для компонентов
Добавляйте @Preview для компонентов. Желательно писать превью не только для базовых компонентов, но и для сложных состояний: с ошибками, лоадингами, иконками, пустым состоянием. Это ускоряет дизайн-ревью и обнаружение багов визуально.Коммуникация с дизайнерами
Регулярно сверяйтесь с дизайнерами и обсуждайте реализацию сложных компонентов. Часто UI можно упростить, не потеряв при этом UX.
4. Антипаттерны
Jetpack Compose делает разработку проще и декларативнее, но с этим приходит и соблазн писать «быстро и работать» без архитектурных основ.
Ниже — распространённые ошибки и плохие практики, которых стоит избегать, чтобы ваш код не превратился в хаос, через месяц.
Лишние remember в компонентах
Избыточное использование remember может привести к трудным для отладки багам. Компонент должен быть как можно более "тупым" (stateless). Если remember не обязателен — не используйте.Modifier внутри компонентов
Не пишитеModifier.padding(...)
прямо внутри компонента без возможности переопределить его. Такой компонент невозможно переиспользовать гибко.Обёртки ради обёрток
Если компонент используется только один раз и имеет уникальную логику — не выносите его. Избыточная абстракция делает код сложнее, а не проще.Переиспользуемость без параметров
Если вы создаёте компонент и “зашиваете” в него цвет, размер и логику — он не сможет использоваться повторно.
5. Краткое заключение
Compose даёт нам не только гибкость, но и ответственность: за архитектуру, читаемость и переиспользуемость. Используйте силу декларативного UI и проектируйте интерфейсы так, чтобы с ними было приятно работать не только пользователям, но и другим разработчикам. А лучше всего — себе через полгода.
kloun_za_2rub
Статья ни о чем, слишком поверхностно, учитывая, что jetpack compose это новый стандарт. Вы еще про spring boot расскажите, какой это современный фреймворк, что такое бины и тд.