Compose Multiplatform - это декларативный UI фреймворк от JetBrains, позволяющий создавать пользовательские интерфейсы для различных платформ (Android, iOS, Desktop, Web) с использованием языка Kotlin.
- Декларативный подход: UI описывается как функция состояния
- Многоплатформенность: один код для разных платформ
- Реактивность: автоматическое обновление UI при изменении данных
- Компонентный подход: переиспользуемые UI-компоненты
fun main() = application {
Window(onCloseRequest = ::exitApplication, title = "Compose App") {
App()
}
}
@Composable
fun App() {
MaterialTheme {
Surface(modifier = Modifier.fillMaxSize()) {
// Содержимое приложения
Text("Привет, Compose Multiplatform!")
}
}
}
@Composable
fun TextExample() {
Column {
Text("Обычный текст")
Text(
text = "Стилизованный текст",
color = Color.Blue,
fontSize = 20.sp,
fontWeight = FontWeight.Bold,
textAlign = TextAlign.Center,
modifier = Modifier.fillMaxWidth()
)
Text(
text = "Текст с переносами и стилями",
style = TextStyle(
fontFamily = FontFamily.Monospace,
lineHeight = 24.sp
)
)
}
}
@Composable
fun ButtonsExample() {
Column(
modifier = Modifier.padding(16.dp),
verticalArrangement = Arrangement.spacedBy(8.dp)
) {
// Обычная кнопка
Button(onClick = { println("Нажата кнопка") }) {
Text("Обычная кнопка")
}
// Кнопка с иконкой
Button(onClick = { /* действие */ }) {
Icon(
Icons.Default.Favorite,
contentDescription = "Избранное",
modifier = Modifier.size(18.dp)
)
Spacer(Modifier.width(8.dp))
Text("Кнопка с иконкой")
}
// Контурная кнопка
OutlinedButton(onClick = { /* действие */ }) {
Text("Контурная кнопка")
}
// Текстовая кнопка
TextButton(onClick = { /* действие */ }) {
Text("Текстовая кнопка")
}
}
}
@Composable
fun ImageExample() {
Column {
// Изображение из ресурсов
Image(
painter = painterResource("sample.png"),
contentDescription = "Пример изображения",
modifier = Modifier.size(100.dp),
contentScale = ContentScale.Crop
)
// Изображение из векторной иконки
Image(
imageVector = Icons.Default.Person,
contentDescription = "Иконка пользователя",
modifier = Modifier.size(50.dp)
)
}
}
@Composable
fun TextFieldExample() {
var text by remember { mutableStateOf("") }
var password by remember { mutableStateOf("") }
Column(modifier = Modifier.padding(16.dp)) {
// Обычное поле ввода
TextField(
value = text,
onValueChange = { text = it },
label = { Text("Введите текст") },
modifier = Modifier.fillMaxWidth()
)
Spacer(modifier = Modifier.height(8.dp))
// Контурное поле ввода
OutlinedTextField(
value = text,
onValueChange = { text = it },
label = { Text("Контурное поле") },
modifier = Modifier.fillMaxWidth()
)
Spacer(modifier = Modifier.height(8.dp))
// Поле для ввода пароля
OutlinedTextField(
value = password,
onValueChange = { password = it },
label = { Text("Пароль") },
visualTransformation = PasswordVisualTransformation(),
keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Password),
modifier = Modifier.fillMaxWidth()
)
}
}
@Composable
fun LayoutExample() {
// Вертикальное расположение (столбец)
Column(
modifier = Modifier.fillMaxWidth().padding(16.dp),
verticalArrangement = Arrangement.spacedBy(8.dp),
horizontalAlignment = Alignment.CenterHorizontally
) {
Text("Элемент 1 в столбце")
Text("Элемент 2 в столбце")
Text("Элемент 3 в столбце")
// Горизонтальное расположение (строка)
Row(
modifier = Modifier.fillMaxWidth().padding(top = 16.dp),
horizontalArrangement = Arrangement.SpaceEvenly,
verticalAlignment = Alignment.CenterVertically
) {
Text("Элемент 1 в строке")
Text("Элемент 2 в строке")
Text("Элемент 3 в строке")
}
}
}
@Composable
fun BoxExample() {
Box(
modifier = Modifier
.size(200.dp)
.background(Color.LightGray)
) {
// Элемент в левом верхнем углу
Text(
"Верхний левый",
modifier = Modifier.align(Alignment.TopStart).padding(8.dp)
)
// Элемент по центру
Box(
modifier = Modifier
.size(100.dp)
.background(Color.White)
.align(Alignment.Center),
contentAlignment = Alignment.Center
) {
Text("Центр")
}
// Элемент в правом нижнем углу
Text(
"Нижний правый",
modifier = Modifier.align(Alignment.BottomEnd).padding(8.dp)
)
}
}
@Composable
fun LazyListExample() {
Column {
// Вертикальный список (аналог RecyclerView)
LazyColumn(
modifier = Modifier.height(200.dp),
verticalArrangement = Arrangement.spacedBy(4.dp),
contentPadding = PaddingValues(horizontal = 16.dp, vertical = 8.dp)
) {
items(50) { index ->
Card(
modifier = Modifier.fillMaxWidth(),
elevation = 2.dp
) {
Text(
"Элемент списка $index",
modifier = Modifier.padding(16.dp)
)
}
}
}
Spacer(modifier = Modifier.height(16.dp))
// Горизонтальный список
LazyRow(
horizontalArrangement = Arrangement.spacedBy(8.dp),
contentPadding = PaddingValues(horizontal = 16.dp)
) {
items(20) { index ->
Card(
modifier = Modifier.size(100.dp),
elevation = 2.dp
) {
Box(contentAlignment = Alignment.Center) {
Text("Элемент $index")
}
}
}
}
}
}
@Composable
fun SelectionControlsExample() {
var checkedState by remember { mutableStateOf(false) }
var selectedOption by remember { mutableStateOf(0) }
var switchState by remember { mutableStateOf(false) }
Column(modifier = Modifier.padding(16.dp)) {
// Checkbox
Row(
verticalAlignment = Alignment.CenterVertically,
modifier = Modifier.clickable { checkedState = !checkedState }
) {
Checkbox(
checked = checkedState,
onCheckedChange = { checkedState = it }
)
Text("Флажок выбора", modifier = Modifier.padding(start = 8.dp))
}
Spacer(modifier = Modifier.height(16.dp))
// RadioButton
val radioOptions = listOf("Вариант 1", "Вариант 2", "Вариант 3")
radioOptions.forEachIndexed { index, text ->
Row(
verticalAlignment = Alignment.CenterVertically,
modifier = Modifier
.clickable { selectedOption = index }
.padding(vertical = 4.dp)
) {
RadioButton(
selected = selectedOption == index,
onClick = { selectedOption = index }
)
Text(text, modifier = Modifier.padding(start = 8.dp))
}
}
Spacer(modifier = Modifier.height(16.dp))
// Switch
Row(
verticalAlignment = Alignment.CenterVertically,
modifier = Modifier.clickable { switchState = !switchState }
) {
Switch(
checked = switchState,
onCheckedChange = { switchState = it }
)
Text("Переключатель", modifier = Modifier.padding(start = 8.dp))
}
}
}
@Composable
fun StateExample() {
// Простое состояние
var counter by remember { mutableStateOf(0) }
// Состояние с несколькими полями
var formState by remember {
mutableStateOf(FormState(name = "", email = "", age = 0))
}
// Список как состояние
var items by remember { mutableStateOf(listOf("Элемент 1", "Элемент 2")) }
Column(modifier = Modifier.padding(16.dp)) {
// Пример использования простого состояния
Text("Счетчик: $counter")
Button(onClick = { counter++ }) {
Text("Увеличить")
}
Spacer(modifier = Modifier.height(16.dp))
// Пример использования состояния формы
OutlinedTextField(
value = formState.name,
onValueChange = { formState = formState.copy(name = it) },
label = { Text("Имя") }
)
OutlinedTextField(
value = formState.email,
onValueChange = { formState = formState.copy(email = it) },
label = { Text("Email") }
)
Spacer(modifier = Modifier.height(16.dp))
// Пример использования списка как состояния
Button(onClick = {
items = items + "Элемент ${items.size + 1}"
}) {
Text("Добавить элемент")
}
LazyColumn {
items(items) { item ->
Text(item, modifier = Modifier.padding(vertical = 4.dp))
}
}
}
}
// Класс для состояния формы
data class FormState(
val name: String,
val email: String,
val age: Int
)
@Composable
fun StateHoistingExample() {
// Состояние поднято на уровень родительского компонента
var text by remember { mutableStateOf("") }
Column(modifier = Modifier.padding(16.dp)) {
// Передаем состояние и обработчик изменений дочернему компоненту
CustomTextField(
value = text,
onValueChange = { text = it },
label = "Введите текст"
)
Spacer(modifier = Modifier.height(16.dp))
Text("Введенный текст: $text")
}
}
@Composable
fun CustomTextField(
value: String,
onValueChange: (String) -> Unit,
label: String
) {
OutlinedTextField(
value = value,
onValueChange = onValueChange,
label = { Text(label) },
modifier = Modifier.fillMaxWidth()
)
}
@Composable
fun DerivedStateExample() {
var text by remember { mutableStateOf("") }
// Производное состояние - вычисляется на основе другого состояния
val isTextValid by remember(text) {
derivedStateOf { text.length >= 3 }
}
Column(modifier = Modifier.padding(16.dp)) {
OutlinedTextField(
value = text,
onValueChange = { text = it },
label = { Text("Введите минимум 3 символа") },
modifier = Modifier.fillMaxWidth(),
isError = !isTextValid && text.isNotEmpty()
)
Spacer(modifier = Modifier.height(8.dp))
if (!isTextValid && text.isNotEmpty()) {
Text(
"Текст должен содержать минимум 3 символа",
color = MaterialTheme.colors.error,
style = MaterialTheme.typography.caption
)
}
Spacer(modifier = Modifier.height(16.dp))
Button(
onClick = { /* действие */ },
enabled = isTextValid
) {
Text("Отправить")
}
}
}
Модификаторы - это ключевой инструмент для настройки внешнего вида и поведения компонентов.
@Composable
fun ModifiersExample() {
Column(modifier = Modifier.padding(16.dp)) {
// Базовые модификаторы размера
Box(
modifier = Modifier
.size(100.dp)
.background(Color.Red)
)
Spacer(modifier = Modifier.height(16.dp))
// Комбинирование модификаторов
Box(
modifier = Modifier
.fillMaxWidth()
.height(50.dp)
.background(Color.Blue)
.padding(8.dp)
.border(2.dp, Color.White)
) {
Text(
"Текст с модификаторами",
color = Color.White,
modifier = Modifier.align(Alignment.Center)
)
}
Spacer(modifier = Modifier.height(16.dp))
// Модификаторы для интерактивности
Box(
modifier = Modifier
.size(100.dp)
.background(Color.Green)
.clickable { println("Клик по зеленому квадрату") }
.padding(8.dp)
) {
Text(
"Кликабельная область",
color = Color.White,
textAlign = TextAlign.Center,
modifier = Modifier.align(Alignment.Center)
)
}
Spacer(modifier = Modifier.height(16.dp))
// Модификаторы для анимации
var expanded by remember { mutableStateOf(false) }
Box(
modifier = Modifier
.animateContentSize()
.background(Color.LightGray)
.clickable { expanded = !expanded }
.height(if (expanded) 200.dp else 50.dp)
.fillMaxWidth()
) {
Text(
if (expanded) "Нажмите, чтобы свернуть" else "Нажмите, чтобы развернуть",
modifier = Modifier
.align(Alignment.Center)
.padding(8.dp)
)
}
}
}
@Composable
fun ThemeExample() {
// Создание собственной темы
MaterialTheme(
colors = lightColors(
primary = Color(0xFF6200EE),
primaryVariant = Color(0xFF3700B3),
secondary = Color(0xFF03DAC6)
),
typography = Typography(
body1 = TextStyle(
fontFamily = FontFamily.Default,
fontWeight = FontWeight.Normal,
fontSize = 16.sp
),
h1 = TextStyle(
fontFamily = FontFamily.Default,
fontWeight = FontWeight.Bold,
fontSize = 30.sp
)
),
shapes = Shapes(
small = RoundedCornerShape(4.dp),
medium = RoundedCornerShape(8.dp),
large = RoundedCornerShape(12.dp)
)
) {
// Контент, использующий тему
Column(modifier = Modifier.padding(16.dp)) {
Text(
"Заголовок с темой",
style = MaterialTheme.typography.h1,
color = MaterialTheme.colors.primary
)
Spacer(modifier = Modifier.height(16.dp))
Surface(
color = MaterialTheme.colors.surface,
elevation = 4.dp,
shape = MaterialTheme.shapes.medium
) {
Text(
"Текст на поверхности с темой",
modifier = Modifier.padding(16.dp),
style = MaterialTheme.typography.body1
)
}
Spacer(modifier = Modifier.height(16.dp))
Button(
onClick = { /* действие */ },
colors = ButtonDefaults.buttonColors(
backgroundColor = MaterialTheme.colors.secondary
),
shape = MaterialTheme.shapes.small
) {
Text("Кнопка с темой")
}
}
}
}
@Composable
fun DarkLightThemeExample() {
var isDarkTheme by remember { mutableStateOf(false) }
// Переключение между темной и светлой темами
MaterialTheme(
colors = if (isDarkTheme) darkColors() else lightColors()
) {
Surface(
modifier = Modifier.fillMaxSize(),
color = MaterialTheme.colors.background
) {
Column(
modifier = Modifier.padding(16.dp),
verticalArrangement = Arrangement.spacedBy(16.dp)
) {
Text(
"Пример ${if (isDarkTheme) "темной" else "светлой"} темы",
style = MaterialTheme.typography.h5
)
Switch(
checked = isDarkTheme,
onCheckedChange = { isDarkTheme = it }
)
Card(
modifier = Modifier.fillMaxWidth(),
elevation = 4.dp
) {
Column(modifier = Modifier.padding(16.dp)) {
Text(
"Карточка с темой",
style = MaterialTheme.typography.h6
)
Spacer(modifier = Modifier.height(8.dp))
Text(
"Текст адаптируется к выбранной теме автоматически"
)
}
}
Button(onClick = { /* действие */ }) {
Text("Кнопка с темой")
}
OutlinedTextField(
value = "",
onValueChange = {},
label = { Text("Поле ввода с темой") }
)
}
}
}
}
@Composable
fun BasicAnimationExample() {
var expanded by remember { mutableStateOf(false) }
// Анимируемые значения
val backgroundColor by animateColorAsState(
if (expanded) Color.Green else Color.LightGray
)
val size by animateDpAsState(
if (expanded) 200.dp else 100.dp
)
Column(
modifier = Modifier.padding(16.dp),
horizontalAlignment = Alignment.CenterHorizontally
) {
Box(
modifier = Modifier
.size(size)
.background(backgroundColor)
.clickable { expanded = !expanded },
contentAlignment = Alignment.Center
) {
Text(
if (expanded) "Нажмите, чтобы уменьшить" else "Нажмите, чтобы увеличить",
textAlign = TextAlign.Center
)
}
}
}
@Composable
fun ContentAnimationExample() {
var currentPage by remember { mutableStateOf(0) }
Column(modifier = Modifier.padding(16.dp)) {
Row(
modifier = Modifier.fillMaxWidth(),
horizontalArrangement = Arrangement.SpaceEvenly
) {
Button(onClick = { currentPage = 0 }) {
Text("Страница 1")
}
Button(onClick = { currentPage = 1 }) {
Text("Страница 2")
}
Button(onClick = { currentPage = 2 }) {
Text("Страница 3")
}
}
Spacer(modifier = Modifier.height(16.dp))
// Анимация при смене контента
AnimatedContent(
targetState = currentPage,
transitionSpec = {
fadeIn() + slideInHorizontally() with
fadeOut() + slideOutHorizontally()
}
) { page ->
when (page) {
0 -> PageContent("Содержимое первой страницы")
1 -> PageContent("Содержимое второй страницы")
2 -> PageContent("Содержимое третьей страницы")
}
}
}
}
@Composable
fun PageContent(text: String) {
Box(
modifier = Modifier
.fillMaxWidth()
.height(200.dp)
.background(Color.LightGray),
contentAlignment = Alignment.Center
) {
Text(
text = text,
fontSize = 20.sp,
textAlign = TextAlign.Center
)
}
}
@Composable
fun MultiplatformExample() {
Column(modifier = Modifier.padding(16.dp)) {
Text("Этот код работает на всех платформах")
Spacer(modifier = Modifier.height(16.dp))
// Общие компоненты
Button(onClick = { /* действие */ }) {
Text("Кнопка для всех платформ")
}
}
}
@Composable
fun PlatformSpecificExample() {
Column(modifier = Modifier.padding(16.dp)) {
Text("Базовый контент для всех платформ")
Spacer(modifier = Modifier.height(16.dp))
// Платформенно-специфичный код
when (getPlatform()) {
Platform.ANDROID -> AndroidSpecificContent()
Platform.IOS -> IOSSpecificContent()
Platform.DESKTOP -> DesktopSpecificContent()
Platform.WEB -> WebSpecificContent()
}
}
}
// Платформенно-специфичные функции
@Composable
expect fun AndroidSpecificContent()
@Composable
expect fun IOSSpecificContent()
@Composable
expect fun DesktopSpecificContent()
@Composable
expect fun WebSpecificContent()
// Реализации для Android
@Composable
actual fun AndroidSpecificContent() {
Text("Контент специфичный для Android")
// Android-специфичные компоненты
}
// Реализации для Desktop
@Composable
actual fun DesktopSpecificContent() {
Text("Контент специфичный для Desktop")
// Desktop-специфичные компоненты
}
// И так далее для других платформ
@Composable
fun NavigationExample() {
var currentScreen by remember { mutableStateOf("home") }
Column {
// Навигационная панель
Row(
modifier = Modifier
.fillMaxWidth()
.background(Color.LightGray)
.padding(8.dp),
horizontalArrangement = Arrangement.SpaceEvenly
) {
NavigationButton(
text = "Главная",
selected = currentScreen == "home",
onClick = { currentScreen = "home" }
)
NavigationButton(
text = "Профиль",
selected = currentScreen == "profile",
onClick = { currentScreen = "profile" }
)
NavigationButton(
text = "Настройки",
selected = currentScreen == "settings",
onClick = { currentScreen = "settings" }
)
}
// Контент экрана
Box(modifier = Modifier.fillMaxSize()) {
when (currentScreen) {
"home" -> HomeScreen()
"profile" -> ProfileScreen()
"settings" -> SettingsScreen()
}
}
}
}
@Composable
fun NavigationButton(
text: String,
selected: Boolean,
onClick: () -> Unit
) {
Button(
onClick = onClick,
colors = ButtonDefaults.buttonColors(
backgroundColor = if (selected) Color.Blue else Color.Gray
)
) {
Text(text, color = Color.White)
}
}
@Composable
fun HomeScreen() {
Box(
modifier = Modifier.fillMaxSize(),
contentAlignment = Alignment.Center
) {
Text("Экран главной страницы", fontSize = 24.sp)
}
}
@Composable
fun ProfileScreen() {
Box(
modifier = Modifier.fillMaxSize(),
contentAlignment = Alignment.Center
) {
Text("Экран профиля", fontSize = 24.sp)
}
}
@Composable
fun SettingsScreen() {
Box(
modifier = Modifier.fillMaxSize(),
contentAlignment = Alignment.Center
) {
Text("Экран настроек", fontSize = 24.sp)
}
}
@Composable
fun GesturesExample() {
var offsetX by remember { mutableStateOf(0f) }
var offsetY by remember { mutableStateOf(0f) }
var scale by remember { mutableStateOf(1f) }
var rotation by remember { mutableStateOf(0f) }
Column(modifier = Modifier.padding(16.dp)) {
// Область для перетаскивания
Box(
modifier = Modifier
.fillMaxWidth()
.height(300.dp)
.background(Color.LightGray)
.padding(16.dp)
) {
// Перетаскиваемый элемент
Box(
modifier = Modifier
.size(100.dp)
.offset { IntOffset(offsetX.roundToInt(), offsetY.roundToInt()) }
.scale(scale)
.rotate(rotation)
.background(Color.Blue)
// Обработка перетаскивания
.pointerInput(Unit) {
detectDragGestures { change, dragAmount ->
change.consume()
offsetX += dragAmount.x
offsetY += dragAmount.y
}
}
// Обработка масштабирования
.pointerInput(Unit) {
detectTransformGestures { _, _, zoom, rotationChange ->
scale *= zoom
rotation += rotationChange
}
}
)
}
Spacer(modifier = Modifier.height(16.dp))
// Кнопка для сброса
Button(
onClick = {
offsetX = 0f
offsetY = 0f
scale = 1f
rotation = 0f
},
modifier = Modifier.align(Alignment.CenterHorizontally)
) {
Text("Сбросить")
}
Spacer(modifier = Modifier.height(16.dp))
// Информация о текущем состоянии
Text("Смещение: X=${offsetX.roundToInt()}, Y=${offsetY.roundToInt()}")
Text("Масштаб: ${String.format("%.2f", scale)}")
Text("Поворот: ${String.format("%.1f", rotation)}°")
}
}
Продолжаю шпаргалку по Compose Multiplatform UI Framework, раздел "Эффекты побочных действий":
@Composable
fun SideEffectsExample() {
var count by remember { mutableStateOf(0) }
// LaunchedEffect - выполняется при первой композиции и при изменении ключа
LaunchedEffect(key1 = count) {
println("LaunchedEffect: count изменился на $count")
// Можно выполнять корутины
delay(1000)
println("Прошла 1 секунда после изменения count")
}
// DisposableEffect - выполняется при первой композиции и очищается при уходе со страницы
DisposableEffect(Unit) {
println("DisposableEffect: компонент добавлен в композицию")
// Возвращаем действие для очистки
onDispose {
println("DisposableEffect: компонент удален из композиции")
}
}
// SideEffect - выполняется при каждой успешной рекомпозиции
SideEffect {
println("SideEffect: произошла рекомпозиция")
}
// rememberCoroutineScope - сохраняет область корутин, привязанную к композиции
val coroutineScope = rememberCoroutineScope()
Column(modifier = Modifier.padding(16.dp)) {
Text("Счетчик: $count")
Spacer(modifier = Modifier.height(16.dp))
Button(onClick = { count++ }) {
Text("Увеличить счетчик")
}
Spacer(modifier = Modifier.height(16.dp))
// Использование coroutineScope для запуска корутин вне композиции
Button(
onClick = {
coroutineScope.launch {
// Действия в корутине
println("Корутина запущена")
delay(2000)
println("Корутина завершена через 2 секунды")
}
}
) {
Text("Запустить корутину")
}
}
}
@Composable
fun LifecycleAwareExample() {
// Получение текущего состояния жизненного цикла
val lifecycleOwner = LocalLifecycleOwner.current
val lifecycle = lifecycleOwner.lifecycle
// Отслеживание изменений жизненного цикла
DisposableEffect(lifecycleOwner) {
val observer = LifecycleEventObserver { _, event ->
when (event) {
Lifecycle.Event.ON_CREATE -> println("ON_CREATE")
Lifecycle.Event.ON_START -> println("ON_START")
Lifecycle.Event.ON_RESUME -> println("ON_RESUME")
Lifecycle.Event.ON_PAUSE -> println("ON_PAUSE")
Lifecycle.Event.ON_STOP -> println("ON_STOP")
Lifecycle.Event.ON_DESTROY -> println("ON_DESTROY")
else -> println("Другое событие: $event")
}
}
lifecycle.addObserver(observer)
onDispose {
lifecycle.removeObserver(observer)
}
}
Box(
modifier = Modifier.fillMaxSize(),
contentAlignment = Alignment.Center
) {
Text("Компонент с отслеживанием жизненного цикла")
}
}
@Composable
fun RecompositionExample() {
var counter by remember { mutableStateOf(0) }
Column(modifier = Modifier.padding(16.dp)) {
Text("Счетчик: $counter")
// Этот компонент будет перекомпозирован при изменении counter
Text("Этот текст будет перекомпозирован: ${System.currentTimeMillis()}")
// Этот компонент НЕ будет перекомпозирован при изменении counter
val stableText = remember {
"Этот текст стабилен: ${System.currentTimeMillis()}"
}
Text(stableText)
Spacer(modifier = Modifier.height(16.dp))
Button(onClick = { counter++ }) {
Text("Увеличить счетчик")
}
Spacer(modifier = Modifier.height(16.dp))
// Оптимизация с помощью key
key(counter / 5) {
Text("Этот текст обновляется каждые 5 нажатий: ${System.currentTimeMillis()}")
}
}
}
@Composable
fun CustomComponentsExample() {
Column(modifier = Modifier.padding(16.dp)) {
// Использование кастомного компонента
CustomCard(
title = "Заголовок карточки",
content = "Содержимое карточки с подробным описанием",
onButtonClick = { println("Кнопка нажата") }
)
Spacer(modifier = Modifier.height(16.dp))
// Еще один кастомный компонент
RatingBar(
rating = 3.5f,
maxRating = 5,
onRatingChanged = { println("Новый рейтинг: $it") }
)
}
}
// Кастомная карточка
@Composable
fun CustomCard(
title: String,
content: String,
onButtonClick: () -> Unit,
modifier: Modifier = Modifier
) {
Card(
modifier = modifier.fillMaxWidth(),
elevation = 4.dp
) {
Column(
modifier = Modifier.padding(16.dp),
verticalArrangement = Arrangement.spacedBy(8.dp)
) {
Text(
text = title,
style = MaterialTheme.typography.h6
)
Text(
text = content,
style = MaterialTheme.typography.body2
)
Button(
onClick = onButtonClick,
modifier = Modifier.align(Alignment.End)
) {
Text("Действие")
}
}
}
}
// Кастомный компонент рейтинга
@Composable
fun RatingBar(
rating: Float,
maxRating: Int = 5,
onRatingChanged: (Float) -> Unit,
modifier: Modifier = Modifier
) {
Row(
modifier = modifier,
horizontalArrangement = Arrangement.spacedBy(4.dp),
verticalAlignment = Alignment.CenterVertically
) {
for (i in 1..maxRating) {
val isFilled = i <= rating
val isHalfFilled = !isFilled && i <= rating + 0.5f
Icon(
imageVector = if (isHalfFilled) Icons.Default.StarHalf
else if (isFilled) Icons.Default.Star
else Icons.Default.StarBorder,
contentDescription = "Звезда $i",
tint = Color.Gold,
modifier = Modifier
.size(24.dp)
.clickable { onRatingChanged(i.toFloat()) }
)
}
Text(
text = String.format("%.1f", rating),
modifier = Modifier.padding(start = 8.dp)
)
}
}
@Composable
fun AdaptiveLayoutExample() {
// Определение текущего размера экрана
val windowInfo = rememberWindowInfo()
Column(modifier = Modifier.padding(16.dp)) {
Text(
"Текущий размер экрана: ${windowInfo.screenWidthInfo}",
style = MaterialTheme.typography.h6
)
Spacer(modifier = Modifier.height(16.dp))
// Адаптивный макет в зависимости от размера экрана
when (windowInfo.screenWidthInfo) {
WindowInfo.WindowType.Compact -> {
// Макет для узких экранов (телефоны)
CompactLayout()
}
WindowInfo.WindowType.Medium -> {
// Макет для средних экранов (планшеты)
MediumLayout()
}
WindowInfo.WindowType.Expanded -> {
// Макет для широких экранов (десктоп)
ExpandedLayout()
}
}
}
}
// Вспомогательный класс для определения размера экрана
class WindowInfo(
val screenWidthInfo: WindowType,
val screenHeightInfo: WindowType
) {
sealed class WindowType {
object Compact : WindowType()
object Medium : WindowType()
object Expanded : WindowType()
}
}
@Composable
fun rememberWindowInfo(): WindowInfo {
val configuration = LocalConfiguration.current
val screenWidth = configuration.screenWidthDp.dp
val screenHeight = configuration.screenHeightDp.dp
return remember(screenWidth, screenHeight) {
WindowInfo(
screenWidthInfo = when {
screenWidth < 600.dp -> WindowInfo.WindowType.Compact
screenWidth < 840.dp -> WindowInfo.WindowType.Medium
else -> WindowInfo.WindowType.Expanded
},
screenHeightInfo = when {
screenHeight < 480.dp -> WindowInfo.WindowType.Compact
screenHeight < 900.dp -> WindowInfo.WindowType.Medium
else -> WindowInfo.WindowType.Expanded
}
)
}
}
@Composable
fun CompactLayout() {
// Вертикальный макет для узких экранов
Column(
modifier = Modifier.fillMaxWidth(),
verticalArrangement = Arrangement.spacedBy(16.dp)
) {
repeat(3) { index ->
Card(
modifier = Modifier.fillMaxWidth(),
elevation = 4.dp
) {
Text(
"Элемент $index для компактного экрана",
modifier = Modifier.padding(16.dp)
)
}
}
}
}
@Composable
fun MediumLayout() {
// Сетка 2x2 для средних экранов
Column(
modifier = Modifier.fillMaxWidth(),
verticalArrangement = Arrangement.spacedBy(16.dp)
) {
Row(
modifier = Modifier.fillMaxWidth(),
horizontalArrangement = Arrangement.spacedBy(16.dp)
) {
Card(
modifier = Modifier.weight(1f),
elevation = 4.dp
) {
Text(
"Элемент 1 для среднего экрана",
modifier = Modifier.padding(16.dp)
)
}
Card(
modifier = Modifier.weight(1f),
elevation = 4.dp
) {
Text(
"Элемент 2 для среднего экрана",
modifier = Modifier.padding(16.dp)
)
}
}
Row(
modifier = Modifier.fillMaxWidth(),
horizontalArrangement = Arrangement.spacedBy(16.dp)
) {
Card(
modifier = Modifier.weight(1f),
elevation = 4.dp
) {
Text(
"Элемент 3 для среднего экрана",
modifier = Modifier.padding(16.dp)
)
}
Card(
modifier = Modifier.weight(1f),
elevation = 4.dp
) {
Text(
"Элемент 4 для среднего экрана",
modifier = Modifier.padding(16.dp)
)
}
}
}
}
@Composable
fun ExpandedLayout() {
// Горизонтальный макет для широких экранов
Row(
modifier = Modifier.fillMaxWidth(),
horizontalArrangement = Arrangement.spacedBy(16.dp)
) {
repeat(3) { index ->
Card(
modifier = Modifier.weight(1f),
elevation = 4.dp
) {
Column(modifier = Modifier.padding(16.dp)) {
Text(
"Элемент $index для широкого экрана",
style = MaterialTheme.typography.h6
)
Spacer(modifier = Modifier.height(8.dp))
Text(
"Дополнительная информация для широкого экрана"
)
}
}
}
}
}
// ViewModel для примера
class ExampleViewModel : ViewModel() {
private val _counter = MutableStateFlow(0)
val counter: StateFlow<Int> = _counter.asStateFlow()
private val _items = MutableStateFlow<List<String>>(emptyList())
val items: StateFlow<List<String>> = _items.asStateFlow()
fun incrementCounter() {
_counter.value++
}
fun addItem(item: String) {
_items.value = _items.value + item
}
}
@Composable
fun ViewModelIntegrationExample() {
// Создание или получение ViewModel
val viewModel = viewModel<ExampleViewModel>()
// Наблюдение за состоянием из ViewModel
val counter by viewModel.counter.collectAsState()
val items by viewModel.items.collectAsState()
Column(modifier = Modifier.padding(16.dp)) {
Text(
"Счетчик: $counter",
style = MaterialTheme.typography.h5
)
Spacer(modifier = Modifier.height(16.dp))
Button(onClick = { viewModel.incrementCounter() }) {
Text("Увеличить счетчик")
}
Spacer(modifier = Modifier.height(16.dp))
var newItem by remember { mutableStateOf("") }
Row(
modifier = Modifier.fillMaxWidth(),
verticalAlignment = Alignment.CenterVertically
) {
OutlinedTextField(
value = newItem,
onValueChange = { newItem = it },
label = { Text("Новый элемент") },
modifier = Modifier.weight(1f)
)
Spacer(modifier = Modifier.width(8.dp))
Button(
onClick = {
if (newItem.isNotEmpty()) {
viewModel.addItem(newItem)
newItem = ""
}
}
) {
Text("Добавить")
}
}
Spacer(modifier = Modifier.height(16.dp))
LazyColumn {
items(items) { item ->
Text(
text = item,
modifier = Modifier
.fillMaxWidth()
.padding(vertical = 8.dp)
)
Divider()
}
}
}
}
@Composable
fun FlowIntegrationExample() {
// Создание Flow
val countDownFlow = flow {
val startingValue = 10
var currentValue = startingValue
emit(startingValue)
while (currentValue > 0) {
delay(1000)
currentValue--
emit(currentValue)
}
}
// Сбор значений из Flow
val countDown by countDownFlow.collectAsState(initial = 10)
// Создание Flow с обновлениями времени
val timeFlow = flow {
while (true) {
emit(System.currentTimeMillis())
delay(1000)
}
}
// Форматирование времени
val dateFormat = remember { SimpleDateFormat("HH:mm:ss", Locale.getDefault()) }
val currentTime by timeFlow.collectAsState(initial = System.currentTimeMillis())
Column(
modifier = Modifier
.fillMaxSize()
.padding(16.dp),
verticalArrangement = Arrangement.Center,
horizontalAlignment = Alignment.CenterHorizontally
) {
Text(
"Обратный отсчет: $countDown",
style = MaterialTheme.typography.h3
)
Spacer(modifier = Modifier.height(32.dp))
Text(
"Текущее время: ${dateFormat.format(Date(currentTime))}",
style = MaterialTheme.typography.h5
)
}
}
@Composable
fun PerformanceTipsExample() {
Column(modifier = Modifier.padding(16.dp)) {
Text(
"Советы по оптимизации производительности:",
style = MaterialTheme.typography.h6
)
Spacer(modifier = Modifier.height(16.dp))
// 1. Используйте remember для кэширования вычислений
val expensiveComputation = remember {
// Здесь может быть дорогостоящее вычисление
"Результат дорогостоящего вычисления"
}
Text("1. Кэшированный результат: $expensiveComputation")
Spacer(modifier = Modifier.height(8.dp))
// 2. Используйте derivedStateOf для вычисляемых состояний
var list by remember { mutableStateOf(List(100) { it }) }
val filteredList by remember(list) {
derivedStateOf { list.filter { it % 2 == 0 } }
}
Text("2. Размер отфильтрованного списка: ${filteredList.size}")
Spacer(modifier = Modifier.height(8.dp))
// 3. Используйте key для контроля рекомпозиции
var counter by remember { mutableStateOf(0) }
key(counter / 10) {
Text("3. Этот текст обновляется каждые 10 изменений: $counter")
}
Spacer(modifier = Modifier.height(8.dp))
// 4. Выносите стабильные компоненты
StableComponent()
Spacer(modifier = Modifier.height(8.dp))
// 5. Используйте LazyColumn вместо Column для больших списков
Text("5. Для больших списков используйте LazyColumn вместо Column")
Spacer(modifier = Modifier.height(16.dp))
Button(onClick = { counter++ }) {
Text("Увеличить счетчик")
}
}
}
// Стабильный компонент, который не будет перекомпозирован при изменении родителя
@Composable
fun StableComponent() {
Text(
"4. Стабильный компонент (не перекомпозируется)",
modifier = Modifier
.fillMaxWidth()
.background(Color.LightGray)
.padding(8.dp)
)
}
@Composable
fun DebuggingExample() {
// Включение отладочной информации
CompositionLocalProvider(LocalInspectionMode provides true) {
Column(modifier = Modifier.padding(16.dp)) {
Text(
"Советы по отладке Compose:",
style = MaterialTheme.typography.h6
)
Spacer(modifier = Modifier.height(16.dp))
// 1. Использование модификатора debugInspectorInfo
Box(
modifier = Modifier
.size(100.dp)
.background(Color.Red)
.debugInspectorInfo {
name = "Красный квадрат"
properties["size"] = "100.dp"
properties["color"] = "Red"
}
)
Spacer(modifier = Modifier.height(16.dp))
// 2. Логирование при рекомпозиции
var counter by remember { mutableStateOf(0) }
if (counter % 2 == 0) {
Text("Счетчик четный: $counter")
SideEffect {
println("Рекомпозиция: счетчик четный - $counter")
}
} else {
Text("Счетчик нечетный: $counter")
SideEffect {
println("Рекомпозиция: счетчик нечетный - $counter")
}
}
Spacer(modifier = Modifier.height(16.dp))
Button(onClick = { counter++ }) {
Text("Увеличить счетчик")
}
}
}
}
Compose Multiplatform - мощный и гибкий фреймворк для создания пользовательских интерфейсов на различных платформах. Ключевые аспекты, которые стоит запомнить:
-
Декларативный подход - UI описывается как функция состояния, что делает код более предсказуемым и легким в поддержке.
-
Компонентный подход - создавайте переиспользуемые компоненты для повышения эффективности разработки.
-
Управление состоянием - используйте
remember
,mutableStateOf
,derivedStateOf
для эффективного управления состоянием. -
Модификаторы - цепочка модификаторов позволяет гибко настраивать внешний вид и поведение компонентов.
-
Эффекты - используйте
LaunchedEffect
,DisposableEffect
,SideEffect
для управления побочными эффектами. -
Оптимизация производительности - применяйте кэширование, ключи и другие техники для оптимизации.
-
Многоплатформенность - используйте общий код где возможно, и платформенно-специфичный код где необходимо.
Эта шпаргалка охватывает основные аспекты Compose Multiplatform, но фреймворк постоянно развивается, поэтому рекомендуется следить за официальной документацией JetBrains для получения актуальной информации.