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 | |
|---|---|---|
| Где | Только Android | Android + Desktop + iOS + … |
| Точка входа | Activity.setContent { } | Зависит от платформы (Window, Activity, …) |
| Общий UI-код | — | В commonMain |
На Android Google поставляет Jetpack Compose; JetBrains развивает общую библиотеку UI для KMP. Синтаксис @Composable, Button, Text для новичка почти одинаковый.
Что получится
Окно Desktop с заголовком, текстом "Счётчик: N" и кнопкой "Увеличить". Нажатия меняют число на экране.
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 |
Что попробовать
- Добавить
OutlinedButton"Сбросить" сcount = 0. - Вынести строку счётчика в отдельную
@Composable fun CounterLabel(count: Int). - Тот же
App()подключить в Android-модуле KMP-проекта.
Дополнительный сниппет — управление состоянием формы
Код ITЗагрузка примера кода…
Разбор:
- Вход: composable принимает ввод пользователя и показывает сохранённое значение.
- Ключевые API:
remember,mutableStateOf,OutlinedTextField,Button, условныйifв UI. - Поток выполнения: пользователь печатает в поле, состояние
nameобновляется, кнопка фиксирует очищенное значение вsavedName. - Результат: экран мгновенно перерисовывается и показывает приветствие после сохранения.
- Типичная ошибка: хранить
nameв обычной переменной безremember; тогда значение сбрасывается при recomposition.
Следующий шаг по архитектуре UI
После первого окна обычно переходят к трём строительным блокам:
- состояние экрана в
StateFlow; - отдельный слой
ViewModel/Presenter; - разделение
commonMain(бизнес-логика) и platform-specific кода.
Это делает проект готовым к масштабированию, когда экранов становится больше двух-трёх.
Дальше
- Ktor Server и Ktor Client — общий backend для всех платформ;
- Jetpack Compose на Android — Activity, ViewModel, навигация;
- мобильный раздел.
В подборках
Статья входит в тематические подборки и блок "С чего начать?" на главной. Соседние шаги того же маршрута:
Первые шаги (маршрут подборки) — Ktor — первая программа, Spring Boot на Kotlin — первая программа, Первая программа на Symfony, Первая программа на Gin, Первая программа на Laravel, Первая программа на Echo.