Compose Multiplatform для Kotlin: Полное руководство
Добро пожаловать в самое подробное руководство по Compose Multiplatform (CMP) — современной технологии от JetBrains, которая позволяет создавать нативные пользовательские интерфейсы для Android, iOS, Desktop (Windows, macOS, Linux) и Web, используя единый код на языке Kotlin. В этой статье мы разберем всё: от установки до продвинутых техник и лучших практик.
1. Что это и зачем нужна?
Compose Multiplatform — это декларативный фреймворк для построения пользовательских интерфейсов, основанный на Jetpack Compose от Google, но расширенный для работы на нескольких платформах. В отличие от традиционных подходов (например, отдельные XML-верстки для Android и SwiftUI для iOS), CMP позволяет писать UI-логику один раз и компилировать её в нативный код для каждой целевой платформы.
Зачем это нужно? Во-первых, это радикальное сокращение времени разработки: вместо поддержки двух или более кодовых баз вы работаете с одной. Во-вторых, консистентность UI: бизнес-логика и внешний вид приложения идентичны на всех платформах, что упрощает тестирование и поддержку. В-третьих, доступ к нативным API: вы можете использовать platform-specific код (например, камеру или геолокацию) через механизм expect/actual, не покидая экосистему Kotlin.
Compose Multiplatform идеально подходит для стартапов, продуктовых команд и разработчиков, которые хотят охватить максимальную аудиторию без дублирования усилий. Библиотека активно развивается JetBrains и сообществом, и на 2025 год является одним из самых перспективных решений в мире кроссплатформенной разработки.
2. Установка
Поскольку Compose Multiplatform — это не библиотека в традиционном смысле (как pip-пакет), а скорее набор плагинов и конфигураций для Gradle, установка производится через систему сборки Kotlin Multiplatform. Вот точная последовательность действий для создания нового проекта:
Шаг 1: Создание проекта через IntelliJ IDEA (рекомендуется)
Откройте IntelliJ IDEA (Community или Ultimate), выберите File → New → Project. В мастере выберите шаблон Compose Multiplatform. Укажите имя проекта и нажмите "Finish". IDE автоматически сгенерирует структуру и все необходимые зависимости.
Шаг 2: Ручная настройка (для существующих проектов)
Добавьте в корневой build.gradle.kts следующий код:
plugins {
kotlin("multiplatform") version "2.0.21"
id("org.jetbrains.compose") version "1.7.1"
id("org.jetbrains.kotlin.plugin.compose") version "2.0.21"
}
kotlin {
androidTarget()
listOf(
iosX64(),
iosArm64(),
iosSimulatorArm64()
).forEach { iosTarget ->
iosTarget.binaries.framework {
baseName = "ComposeApp"
isStatic = true
}
}
sourceSets {
val commonMain by getting {
dependencies {
implementation(compose.runtime)
implementation(compose.foundation)
implementation(compose.material3)
implementation(compose.ui)
}
}
val androidMain by getting {
dependencies {
implementation("androidx.activity:activity-compose:1.9.3")
}
}
}
}
Важно: Версии плагинов и Kotlin должны быть совместимы. Актуальные версии всегда можно найти на GitHub репозитории Compose Multiplatform.
3. Быстрый старт — минимальный рабочий пример
После создания проекта откройте файл commonMain/kotlin/App.kt и замените его содержимое на следующий код:
import androidx.compose.foundation.layout.*
import androidx.compose.material3.*
import androidx.compose.runtime.*
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.dp
@Composable
fun App() {
var count by remember { mutableStateOf(0) }
MaterialTheme {
Column(
modifier = Modifier.fillMaxSize().padding(16.dp),
horizontalAlignment = Alignment.CenterHorizontally,
verticalArrangement = Arrangement.Center
) {
Text("Вы нажали кнопку $count раз")
Spacer(Modifier.height(16.dp))
Button(onClick = { count++ }) {
Text("Нажми меня")
}
}
}
}
Этот код создаёт простейший счётчик: текст, отображающий количество нажатий, и кнопку. Чтобы запустить приложение на Android, используйте таск ./gradlew installDebug; для Desktop — ./gradlew run; для iOS — откройте Xcode проект и запустите симулятор.
4. Основные методы, функции и классы
Ниже приведены ключевые компоненты Compose Multiplatform с сигнатурами, описанием и примерами. Все примеры можно скопировать и вставить в ваш проект.
4.1. @Composable
Сигнатура: @Composable annotation
Что делает: Аннотация, которая указывает, что функция является компонуемой — то есть может быть вызвана только из контекста композиции и может читать/изменять состояние.
Пример:
@Composable
fun Greeting(name: String) {
Text("Привет, $name!")
}
4.2. Column
Сигнатура: @Composable fun Column(
modifier: Modifier = Modifier,
verticalArrangement: Arrangement.Vertical = Arrangement.Top,
horizontalAlignment: Alignment.Horizontal = Alignment.Start,
content: @Composable ColumnScope.() -> Unit
)
Что делает: Размещает дочерние элементы вертикально сверху вниз. Аналог LinearLayout с вертикальной ориентацией.
Пример:
Column(
modifier = Modifier.fillMaxWidth().padding(16.dp),
verticalArrangement = Arrangement.spacedBy(8.dp)
) {
Text("Первый элемент")
Text("Второй элемент")
Text("Третий элемент")
}
4.3. Row
Сигнатура: @Composable fun Row(
modifier: Modifier = Modifier,
horizontalArrangement: Arrangement.Horizontal = Arrangement.Start,
verticalAlignment: Alignment.Vertical = Alignment.Top,
content: @Composable RowScope.() -> Unit
)
Что делает: Размещает дочерние элементы горизонтально слева направо. Аналог LinearLayout с горизонтальной ориентацией.
Пример:
Row(
modifier = Modifier.fillMaxWidth(),
horizontalArrangement = Arrangement.SpaceEvenly
) {
Button(onClick = { /* действие 1 */ }) { Text("A") }
Button(onClick = { /* действие 2 */ }) { Text("B") }
Button(onClick = { /* действие 3 */ }) { Text("C") }
}
4.4. Box
Сигнатура: @Composable fun Box(
modifier: Modifier = Modifier,
contentAlignment: Alignment = Alignment.TopStart,
propagateMinConstraints: Boolean = false,
content: @Composable BoxScope.() -> Unit
)
Что делает: Контейнер, который накладывает дочерние элементы друг на друга (как FrameLayout). Полезно для создания наложений, бейджей и т.д.
Пример:
Box(modifier = Modifier.size(200.dp)) {
Image(
painter = painterResource(Res.drawable.background),
contentDescription = "Фон",
modifier = Modifier.fillMaxSize()
)
Text(
"Текст поверх картинки",
modifier = Modifier.align(Alignment.Center),
color = Color.White
)
}
4.5. remember
Сигнатура: @Composable inline fun <T> remember(calculation: @DisallowComposableCalls () -> T): T
Что делает: Сохраняет значение между рекомпозициями. Если ключи (переданные как аргументы) не изменились, возвращает сохранённое значение, иначе пересчитывает.
Пример:
val randomNumber = remember { Random.nextInt(1, 100) }
Text("Случайное число: $randomNumber") // не изменится при рекомпозиции
4.6. mutableStateOf
Сигнатура: fun <T> mutableStateOf(value: T, policy: SnapshotMutationPolicy<T> = structuralEqualityPolicy()): MutableState<T>
Что делает: Создаёт наблюдаемое состояние. При изменении значения все компонуемые функции, которые его читают, автоматически перерисовываются.
Пример:
var text by remember { mutableStateOf("") }
TextField(
value = text,
onValueChange = { text = it },
label = { Text("Введите текст") }
)
4.7. LaunchedEffect
Сигнатура: @Composable fun LaunchedEffect(
vararg keys: Any?,
block: suspend CoroutineScope.() -> Unit
)
Что делает: Запускает корутину в области композиции. Автоматически отменяет её при выходе из композиции или изменении ключей. Идеально для side-эффектов (загрузка данных, таймеры).
Пример:
var seconds by remember { mutableStateOf(0) }
LaunchedEffect(Unit) {
while (true) {
delay(1000)
seconds++
}
}
Text("Прошло $seconds секунд")
4.8. Modifier
Сигнатура: sealed class Modifier (основные методы: .fillMaxSize(), .padding(), .clickable(), .background(), .size() и т.д.)
Что делает: Объект, который модифицирует внешний вид и поведение компонуемого элемента. Модификаторы применяются цепочкой.
Пример:
Text(
"Нажми на меня",
modifier = Modifier
.clickable { println("Клик!") }
.padding(16.dp)
.background(Color.LightGray, shape = RoundedCornerShape(8.dp))
.fillMaxWidth()
)
4.9. LazyColumn
Сигнатура: @Composable fun LazyColumn(
modifier: Modifier = Modifier,
state: LazyListState = rememberLazyListState(),
contentPadding: PaddingValues = PaddingValues(0.dp),
reverseLayout: Boolean = false,
verticalArrangement: Arrangement.Vertical = Arrangement.Top,
horizontalAlignment: Alignment.Horizontal = Alignment.Start,
flingBehavior: FlingBehavior = ScrollableDefaults.flingBehavior(),
userScrollEnabled: Boolean = true,
content: LazyListScope.() -> Unit
)
Что делает: Вертикальный список с ленивой загрузкой элементов. Отображает только видимые элементы, что критично для производительности при больших списках.
Пример:
val items = (1..1000).toList()
LazyColumn {
items(items) { item ->
Text("Элемент #$item", modifier = Modifier.padding(8.dp))
}
}
4.10. Scaffold
Сигнатура: @Composable fun Scaffold(
modifier: Modifier = Modifier,
topBar: @Composable () -> Unit = {},
bottomBar: @Composable () -> Unit = {},
snackbarHost: @Composable () -> Unit = {},
floatingActionButton: @Composable () -> Unit = {},
content: @Composable (PaddingValues) -> Unit
)
Что делает: Реализует базовую структуру Material Design: верхняя панель, нижняя панель, плавающая кнопка, снэкбар. Автоматически управляет отступами.
Пример:
Scaffold(
topBar = {
TopAppBar(title = { Text("Мое приложение") })
},
floatingActionButton = {
FloatingActionButton(onClick = { /* действие */ }) {
Icon(Icons.Default.Add, contentDescription = "Добавить")
}
}
) { paddingValues ->
Text("Контент", modifier = Modifier.padding(paddingValues))
}
5. Реальный практический пример (100-200 строк кода)
Давайте создадим полноценное приложение — список задач (Todo List) с возможностью добавления, удаления и отметки выполнения. Этот пример демонстрирует работу с состоянием, списками, вводом текста и модификаторами.
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.items
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Add
import androidx.compose.material.icons.filled.Check
import androidx.compose.material.icons.filled.Delete
import androidx.compose.material3.*
import androidx.compose.runtime.*
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.text.input.TextFieldValue
import androidx.compose.ui.unit.dp
data class TodoItem(
val id: Int,
val text: String,
val isCompleted: Boolean = false
)
@Composable
fun TodoApp() {
var todos by remember { mutableStateOf(listOf<TodoItem>