Перейти к основному содержимому

Compose Multiplatform — первая программа

Разработчику Архитектору

Compose Multiplatform — первая программа

Compose Multiplatform (CMP) — способ писать интерфейс на Kotlin и запускать его на нескольких платформах — Android, Desktop (Windows/macOS/Linux), iOS, Web (экспериментально). Вы описываете экран функциями, а не XML или отдельными файлами разметки для каждой ОС.

Эта статья — первый рабочий пример: окно на Desktop со счётчиком. После него проще перейти к Android (Jetpack Compose) или к общему backend (Ktor).

База Kotlin: Первая программа · Корутины · Экосистема.


Словарь терминов

ТерминПростыми словами
UI (интерфейс)Кнопки, текст, поля ввода — всё, что видит пользователь.
Декларативный UIВы пишете "как экран должен выглядеть при текущем состоянии", а фреймворк сам обновляет картинку при изменении данных.
@ComposableФункция, которая рисует кусок UI. Вызывается только из другой @Composable или из setContent / application { }.
RecompositionПерерисовка части UI, когда изменилось состояние (например, число на счётчике).
KMP (Kotlin Multiplatform)Один проект Gradle с общим кодом (commonMain) и платформенными частями (desktopMain, androidMain).
remember"Запомнить значение между перерисовками", иначе при каждом обновлении экрана счётчик сбросился бы в 0.
MaterialThemeНабор цветов, шрифтов и стилей Material Design.

Чем CMP отличается от "просто Android Compose"

Jetpack Compose (Android)Compose Multiplatform
ГдеТолько AndroidAndroid + Desktop + iOS + …
Точка входаActivity.setContent { }Зависит от платформы (Window, Activity, …)
Общий UI-кодВ commonMain

На Android Google поставляет Jetpack Compose; JetBrains развивает общую библиотеку UI для KMP. Синтаксис @Composable, Button, Text для новичка почти одинаковый.


Что получится

Окно Desktop с заголовком, текстом "Счётчик: N" и кнопкой "Увеличить". Нажатия меняют число на экране.

Зачем начинать с Desktop

Desktop-цель ускоряет первый запуск — без эмулятора и мобильной сборки видно, как работает @Composable, состояние и recomposition. После этого тот же App() проще перенести на Android/iOS.


Создание проекта

Android Studio: File → New → Project → Kotlin Multiplatform App.

Или шаблон на kmp.jetbrains.com.

Для первого раза достаточно цели Desktop (JVM) — не нужен эмулятор Android.

Структура (упрощённо):

project/
├── build.gradle.kts
├── src/
│ ├── commonMain/kotlin/ ← общий UI (App.kt)
│ └── desktopMain/kotlin/ ← main() и Window

Разбор:

  • Вход: показана базовая структура KMP-проекта с общим и платформенным кодом.
  • Ключевые API: commonMain и desktopMain выступают как source set'ы для разных слоёв.
  • Поток выполнения: общий UI складывается в commonMain, а запуск окна и Desktop-обвязка живут в desktopMain.
  • Результат: код интерфейса можно переиспользовать между платформами без дублирования.
  • Типичная ошибка: держать весь код только в desktopMain и потерять мультиплатформенность.

Зависимости (build.gradle.kts, фрагмент)

Код ITЗагрузка примера кода…

Разбор:

  • блок plugins подключает KMP и Compose-плагины, без них Gradle не соберёт UI target.
  • jvm("desktop") создаёт Desktop-target, где код выполняется на JVM.
  • commonMain получает базовые библиотеки Compose для общего интерфейса.
  • desktopMain подключает адаптер Compose под текущую операционную систему.
  • mainClass = "MainKt" указывает стартовый класс с функцией main.
СтрокаСмысл
kotlin("multiplatform")Включает KMP: несколько target'ов в одном модуле.
jvm("desktop")Цель "Desktop на JVM" — обычная Java-программа с окном.
commonMain + compose.*UI-библиотеки доступны во всём общем коде.
compose.desktop.currentOsЗапуск окна на вашей ОС.
mainClass = "MainKt"Файл main.kt с fun main() компилируется в класс MainKt.

Общий UI — commonMain/App.kt

Код ITЗагрузка примера кода…

Разбор:

  • @Composable fun App() описывает UI как функцию от состояния.
  • remember { mutableStateOf(0) } хранит счётчик между recomposition.
  • MaterialTheme задаёт визуальную тему для вложенных элементов.
  • Column формирует вертикальную компоновку, а Modifier задаёт размер и отступы.
  • Button(onClick = { count++ }) меняет состояние, после чего Compose обновляет текст со счётчиком.

Разбор по блокам

Состояние счётчика

var count by remember { mutableStateOf(0) }

Разбор:

  • mutableStateOf(0) создаёт наблюдаемое состояние со стартом в нуле.

  • remember предотвращает сброс значения при перерисовке composable.

  • делегат by даёт прямой доступ к значению без .value.

  • mutableStateOf(0) — обёртка над числом; при присвоении count++ Compose узнаёт, что нужно перерисовать экран.

  • remember { ... } — создать состояние один раз при первом показе App(), хранить между recomposition.

  • by — делегат: пишете count++ вместо count.value++.

Вёрстка

Column(
modifier = Modifier.fillMaxSize().padding(24.dp),
verticalArrangement = Arrangement.spacedBy(12.dp)
) { ... }

Разбор:

  • fillMaxSize() растягивает контейнер на всё окно.

  • padding(24.dp) даёт внутренний отступ от края.

  • spacedBy(12.dp) задаёт равномерный интервал между строками контента.

  • итог — читаемая базовая layout-сетка без ручных расчётов координат.

  • Column — элементы друг под другом (как вертикальный LinearLayout в Android).

  • Modifier.fillMaxSize().padding(24.dp) — занять всё окно, отступ 24 density-independent pixels.

  • Arrangement.spacedBy(12.dp) — зазор 12 dp между дочерними элементами.

Кнопка

Button(onClick = { count++ }) {
Text("Увеличить")
}

Разбор:

  • onClick принимает лямбду, которая выполняется по нажатию кнопки.
  • count++ обновляет state, что автоматически инициирует обновление UI.
  • вложенный Text определяет подпись кнопки в декларативной форме.

onClick — лямбда без параметров: при нажатии увеличить count. Внутри Button — дочерний Text (подпись на кнопке).


Точка входа Desktop — desktopMain/main.kt


import androidx.compose.ui.window.Window
import androidx.compose.ui.window.application

fun main() = application {
Window(onCloseRequest = ::exitApplication, title = "Hello CMP") {
App()
}
}

Разбор:

  • application { ... } запускает event loop Desktop-приложения.
  • Window(...) создаёт нативное окно и задаёт его заголовок.
  • ::exitApplication передаётся как ссылка на функцию корректного завершения.
  • вызов App() монтирует общий UI в содержимое окна.
ЭлементРоль
application { }Цикл приложения Desktop Compose (события окна, завершение).
Window(...)Одно окно ОС с заголовком Hello CMP.
onCloseRequest = ::exitApplicationПри крестике — корректный выход.
App()Ваш общий UI из commonMain.

Запуск:

./gradlew :desktopRun

Разбор:

  • команда вызывает задачу запуска Desktop-target через Gradle Wrapper.
  • префикс : указывает задачу модуля/проекта в Gradle namespace.
  • перед стартом выполняется сборка и проверка зависимостей.

Точное имя задачи зависит от шаблона; в IDE — Run configuration для desktop target.


Платформы CMP

TargetСтатусКомментарий
AndroidСтабильноТот же UI в commonMain, вход через Activity
Desktop JVMСтабильноКак в этой статье
iOSСтабильноНативный рендер, сборка через Xcode
WebЭкспериментальноWasm/JS — следите за релиз-нотами

Практика KMP: бизнес-логику (расчёты, модели, клиент API) держите в commonMain; доступ к файлам, камере, уведомлениям — в androidMain / iosMain / desktopMain через expect/actual.


Связь с корутинами и сетью

Счётчик локальный — сеть не нужна. Для загрузки данных с сервера в commonMain используют Ktor Client и корутины; результат кладут в mutableStateOf или StateFlow и читают в @Composable.


Частые ошибки

СимптомПричина
@Composable invocations can only happen...Вызвали App() из обычной функции без @Composable
Счётчик всегда 0Забыли remember
Gradle не находит desktopRunДругое имя target в шаблоне — смотрите задачи в IDE
Красный syncНесовместимые версии Kotlin и Compose — выровняйте по документации JetBrains

Что попробовать

  1. Добавить OutlinedButton "Сбросить" с count = 0.
  2. Вынести строку счётчика в отдельную @Composable fun CounterLabel(count: Int).
  3. Тот же App() подключить в Android-модуле KMP-проекта.

Дополнительный сниппет — управление состоянием формы

Код ITЗагрузка примера кода…

Разбор:

  • Вход: composable принимает ввод пользователя и показывает сохранённое значение.
  • Ключевые API: remember, mutableStateOf, OutlinedTextField, Button, условный if в UI.
  • Поток выполнения: пользователь печатает в поле, состояние name обновляется, кнопка фиксирует очищенное значение в savedName.
  • Результат: экран мгновенно перерисовывается и показывает приветствие после сохранения.
  • Типичная ошибка: хранить name в обычной переменной без remember; тогда значение сбрасывается при recomposition.

Следующий шаг по архитектуре UI

После первого окна обычно переходят к трём строительным блокам:

  1. состояние экрана в StateFlow;
  2. отдельный слой ViewModel/Presenter;
  3. разделение commonMain (бизнес-логика) и platform-specific кода.

Это делает проект готовым к масштабированию, когда экранов становится больше двух-трёх.


Дальше


В подборках

Статья входит в тематические подборки и блок "С чего начать?" на главной. Соседние шаги того же маршрута:

Первые шаги (маршрут подборки) — Ktor — первая программа, Spring Boot на Kotlin — первая программа, Первая программа на Symfony, Первая программа на Gin, Первая программа на Laravel, Первая программа на Echo.