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 и в крупных проектах: 

  1. Обязательные параметры данных
    То, без чего компонент не имеет смысла: текст, изображение и т.д.

  2. Callbacks (действия)
    Обработчики кликов, изменений, интеракций.

  3. Modifier
    Обязательно передается последним из "содержательных" параметров, с дефолтом.

  4. Опциональные параметры управления (State)
    Например, состояния загрузки, ошибок, видимости.

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

Комментарии (1)


  1. kloun_za_2rub
    11.06.2025 21:32

    Статья ни о чем, слишком поверхностно, учитывая, что jetpack compose это новый стандарт. Вы еще про spring boot расскажите, какой это современный фреймворк, что такое бины и тд.