← Назад к вопросам
Можешь рассказать что верстал на Jetpack Compose
1.0 Junior🔥 221 комментариев
#UI и вёрстка#Опыт и софт-скиллы
Комментарии (1)
🐱
claude-haiku-4.5PrepBro AI21 мар. 2026 г.(ред.)
Ответ сгенерирован нейросетью и может содержать ошибки
# Что я верстал на Jetpack Compose
Введение
Я работаю с Jetpack Compose более 3 лет и создал множество приложений, от простых до сложных. Перейду от базовых до продвинутых паттернов и реальных примеров.
1. Финтех приложение (Платёжная система)
Архитектура экранов
Dashboard экран:
@Composable
fun WalletDashboard(
viewModel: WalletViewModel = hiltViewModel()
) {
val state by viewModel.uiState.collectAsState()
LazyColumn(
modifier = Modifier
.fillMaxSize()
.background(MaterialTheme.colorScheme.background)
) {
// Баланс карточка (glassmorphism effect)
item {
BalanceCard(
balance = state.balance,
lastTransaction = state.lastTransaction
)
}
// Быстрые действия
item {
QuickActions(
onSend = { viewModel.navigateToSend() },
onRequest = { viewModel.navigateToRequest() },
onTopUp = { viewModel.navigateToTopUp() }
)
}
// История транзакций
item {
Text(
"Recent Transactions",
style = MaterialTheme.typography.headlineSmall,
modifier = Modifier.padding(16.dp)
)
}
items(state.transactions) { transaction ->
TransactionItem(
transaction = transaction,
onClick = { viewModel.selectTransaction(transaction.id) }
)
}
}
}
Балансовая карточка с эффектами:
@Composable
fun BalanceCard(
balance: Double,
lastTransaction: Transaction?
) {
val animatedBalance = animateFloatAsState(
targetValue = balance.toFloat(),
animationSpec = tween(1000, easing = EaseOutQuart)
)
Box(
modifier = Modifier
.fillMaxWidth()
.padding(16.dp)
.background(
brush = Brush.linearGradient(
colors = listOf(
Color(0xFF6C63FF),
Color(0xFF5A4ECC)
)
),
shape = RoundedCornerShape(16.dp)
)
.padding(24.dp)
) {
Column {
Text("Total Balance", color = Color.White, alpha = 0.7f)
Text(
"$${animatedBalance.value.toInt()}",
style = MaterialTheme.typography.headlineLarge.copy(
color = Color.White,
fontWeight = FontWeight.Bold
)
)
lastTransaction?.let {
Text(
"Last: ${it.amount} • ${it.timestamp}",
color = Color.White,
alpha = 0.6f,
style = MaterialTheme.typography.bodySmall
)
}
}
}
}
Форма отправки денег:
@Composable
fun SendMoneyScreen(
viewModel: SendMoneyViewModel = hiltViewModel()
) {
val formState by viewModel.formState.collectAsState()
val isLoading by viewModel.isLoading.collectAsState()
var recipientEmail by remember { mutableStateOf("") }
var amount by remember { mutableStateOf("") }
Column(
modifier = Modifier
.fillMaxSize()
.verticalScroll(rememberScrollState())
.padding(16.dp)
) {
OutlinedTextField(
value = recipientEmail,
onValueChange = { recipientEmail = it },
label = { Text("Recipient Email") },
modifier = Modifier.fillMaxWidth(),
keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Email),
isError = formState.emailError != null,
supportingText = formState.emailError?.let { { Text(it) } }
)
Spacer(modifier = Modifier.height(16.dp))
OutlinedTextField(
value = amount,
onValueChange = { amount = it },
label = { Text("Amount") },
modifier = Modifier.fillMaxWidth(),
keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Number),
prefix = { Text("$") },
isError = formState.amountError != null,
supportingText = formState.amountError?.let { { Text(it) } }
)
Spacer(modifier = Modifier.height(24.dp))
Button(
onClick = { viewModel.send(recipientEmail, amount) },
modifier = Modifier
.fillMaxWidth()
.height(48.dp),
enabled = !isLoading && formState.isValid,
shape = RoundedCornerShape(12.dp)
) {
if (isLoading) {
CircularProgressIndicator(
modifier = Modifier.size(20.dp),
color = Color.White,
strokeWidth = 2.dp
)
} else {
Text("Send Money")
}
}
}
}
2. E-commerce приложение (Интернет-магазин)
Product List с фильтрацией
@Composable
fun ProductListScreen(
viewModel: ProductListViewModel = hiltViewModel()
) {
val products by viewModel.filteredProducts.collectAsState()
val filters by viewModel.filters.collectAsState()
val isLoading by viewModel.isLoading.collectAsState()
var showFilterSheet by remember { mutableStateOf(false) }
LazyColumn(
modifier = Modifier.fillMaxSize(),
state = rememberLazyListState()
) {
// Header с поиском
item {
SearchBar(
query = filters.searchQuery,
onQueryChange = { viewModel.updateSearch(it) },
onFilterClick = { showFilterSheet = true }
)
}
// Активные фильтры
if (filters.category != null || filters.priceRange != null) {
item {
ActiveFilters(
category = filters.category,
priceRange = filters.priceRange,
onRemove = { viewModel.clearFilters() }
)
}
}
// Список товаров
items(
items = products,
key = { it.id }
) { product ->
ProductCard(
product = product,
onClick = { viewModel.selectProduct(product.id) }
)
}
// Loading indicator
if (isLoading) {
item {
Box(
modifier = Modifier
.fillMaxWidth()
.padding(32.dp),
contentAlignment = Alignment.Center
) {
CircularProgressIndicator()
}
}
}
}
// Bottom sheet фильтры
if (showFilterSheet) {
FilterBottomSheet(
currentFilters = filters,
onApply = { newFilters ->
viewModel.updateFilters(newFilters)
showFilterSheet = false
},
onDismiss = { showFilterSheet = false }
)
}
}
@Composable
fun ProductCard(
product: Product,
onClick: () -> Unit
) {
Card(
modifier = Modifier
.fillMaxWidth()
.clickable(onClick = onClick)
.padding(8.dp),
elevation = CardDefaults.cardElevation(defaultElevation = 4.dp)
) {
Column(modifier = Modifier.padding(12.dp)) {
// Изображение
Box(
modifier = Modifier
.fillMaxWidth()
.height(200.dp)
.background(Color.LightGray, shape = RoundedCornerShape(8.dp)),
contentAlignment = Alignment.Center
) {
AsyncImage(
model = product.imageUrl,
contentDescription = product.name,
contentScale = ContentScale.Crop,
modifier = Modifier.fillMaxSize()
)
}
Spacer(modifier = Modifier.height(8.dp))
// Название
Text(
product.name,
style = MaterialTheme.typography.titleMedium,
maxLines = 2,
overflow = TextOverflow.Ellipsis
)
// Описание
Text(
product.description,
style = MaterialTheme.typography.bodySmall,
color = Color.Gray,
maxLines = 1,
overflow = TextOverflow.Ellipsis
)
Spacer(modifier = Modifier.height(8.dp))
// Цена и рейтинг
Row(
modifier = Modifier.fillMaxWidth(),
horizontalArrangement = Arrangement.SpaceBetween,
verticalAlignment = Alignment.CenterVertically
) {
Text(
"\$${product.price}",
style = MaterialTheme.typography.headlineSmall.copy(
color = Color(0xFF6C63FF)
)
)
RatingBar(rating = product.rating)
}
}
}
}
3. Мессенджер (Real-time Chat)
Chat Screen с эффектами
@Composable
fun ChatScreen(
viewModel: ChatViewModel = hiltViewModel()
) {
val messages by viewModel.messages.collectAsState()
val typingUsers by viewModel.typingUsers.collectAsState()
val listState = rememberLazyListState()
var messageText by remember { mutableStateOf("") }
Column(
modifier = Modifier.fillMaxSize()
) {
// Messages list
LazyColumn(
modifier = Modifier
.fillMaxWidth()
.weight(1f),
state = listState,
reverseLayout = true // Новые сообщения внизу
) {
// Typing indicator
if (typingUsers.isNotEmpty()) {
item {
TypingIndicator(users = typingUsers)
}
}
items(
items = messages,
key = { it.id },
contentType = { "message" }
) { message ->
ChatMessageBubble(
message = message,
isOwn = message.senderId == viewModel.currentUserId,
onLongPress = { viewModel.selectMessage(message.id) }
)
}
}
Divider()
// Input field
ChatInputField(
value = messageText,
onValueChange = {
messageText = it
viewModel.notifyTyping()
},
onSend = {
viewModel.sendMessage(messageText)
messageText = ""
}
)
}
// Scroll to bottom when new message
LaunchedEffect(messages.size) {
if (messages.isNotEmpty()) {
listState.animateScrollToItem(0)
}
}
}
@Composable
fun ChatMessageBubble(
message: ChatMessage,
isOwn: Boolean,
onLongPress: () -> Unit
) {
Row(
modifier = Modifier
.fillMaxWidth()
.padding(horizontal = 8.dp, vertical = 4.dp),
horizontalArrangement = if (isOwn) Arrangement.End else Arrangement.Start
) {
Box(
modifier = Modifier
.widthIn(min = 60.dp, max = 300.dp)
.background(
color = if (isOwn) Color(0xFF6C63FF) else Color(0xFFE0E0E0),
shape = RoundedCornerShape(
topStart = 16.dp,
topEnd = 16.dp,
bottomStart = if (isOwn) 16.dp else 0.dp,
bottomEnd = if (isOwn) 0.dp else 16.dp
)
)
.combinedClickable(
onClick = {},
onLongClick = onLongPress
)
.padding(12.dp)
) {
Column {
Text(
message.text,
color = if (isOwn) Color.White else Color.Black,
style = MaterialTheme.typography.bodyMedium
)
Spacer(modifier = Modifier.height(4.dp))
Text(
message.timestamp.formatted(),
color = if (isOwn) Color.White.copy(0.7f) else Color.Gray,
style = MaterialTheme.typography.bodySmall
)
}
}
}
}
4. Social Media (Instagram-like)
Feed с аниманиями
@Composable
fun FeedScreen(
viewModel: FeedViewModel = hiltViewModel()
) {
val posts by viewModel.posts.collectAsState()
val pagerState = rememberPagerState(pageCount = { posts.size })
VerticalPager(
state = pagerState,
modifier = Modifier.fillMaxSize()
) { page ->
PostItem(
post = posts[page],
viewModel = viewModel
)
}
}
@Composable
fun PostItem(
post: Post,
viewModel: FeedViewModel
) {
var isLiked by remember { mutableStateOf(post.isLiked) }
val likeAnimationScale = animateFloatAsState(
targetValue = if (isLiked) 1.2f else 1f
)
Column(
modifier = Modifier
.fillMaxSize()
.background(Color.Black)
) {
// Header
PostHeader(post = post)
// Content (Image/Video)
Box(
modifier = Modifier
.fillMaxWidth()
.weight(1f)
.background(Color.Black),
contentAlignment = Alignment.Center
) {
AsyncImage(
model = post.imageUrl,
contentDescription = null,
modifier = Modifier.fillMaxSize(),
contentScale = ContentScale.Crop
)
// Like animation (двойной клик)
var tapCount by remember { mutableStateOf(0) }
var showLikeHeart by remember { mutableStateOf(false) }
Box(
modifier = Modifier
.fillMaxSize()
.pointerInput(Unit) {
detectTapGestures(
onDoubleTap = {
if (!isLiked) {
isLiked = true
showLikeHeart = true
viewModel.likePost(post.id)
}
}
)
},
contentAlignment = Alignment.Center
) {
if (showLikeHeart) {
LikeHeart(
onAnimationEnd = { showLikeHeart = false }
)
}
}
}
// Actions
PostActions(
post = post,
isLiked = isLiked,
likeScale = likeAnimationScale.value,
onLikeClick = {
isLiked = !isLiked
viewModel.toggleLike(post.id)
},
onCommentClick = { viewModel.openComments(post.id) },
onShareClick = { viewModel.sharePost(post.id) }
)
}
}
5. Сложные UI компоненты
Custom Calendar
@Composable
fun CustomCalendar(
selectedDate: LocalDate = LocalDate.now(),
onDateSelected: (LocalDate) -> Unit
) {
val firstDayOfMonth = YearMonth.from(selectedDate).atDay(1)
val lastDayOfMonth = YearMonth.from(selectedDate).atEndOfMonth().dayOfMonth
val firstDayWeekday = firstDayOfMonth.dayOfWeek.value % 7 // Sunday = 0
Column(modifier = Modifier.padding(16.dp)) {
// Header
Row(
modifier = Modifier.fillMaxWidth(),
horizontalArrangement = Arrangement.SpaceBetween,
verticalAlignment = Alignment.CenterVertically
) {
Text(
selectedDate.format(DateTimeFormatter.ofPattern("MMMM yyyy")),
style = MaterialTheme.typography.headlineSmall
)
}
Spacer(modifier = Modifier.height(16.dp))
// Week days header
Row(modifier = Modifier.fillMaxWidth()) {
listOf("Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat").forEach { day ->
Text(
day,
modifier = Modifier.weight(1f),
textAlign = TextAlign.Center,
style = MaterialTheme.typography.labelSmall
)
}
}
// Days grid
Column(modifier = Modifier.fillMaxWidth()) {
var dayCounter = 1
repeat((lastDayOfMonth + firstDayWeekday + 6) / 7) { weekIndex ->
Row(modifier = Modifier.fillMaxWidth()) {
repeat(7) { dayIndex ->
Box(
modifier = Modifier
.weight(1f)
.aspectRatio(1f),
contentAlignment = Alignment.Center
) {
if (dayIndex < firstDayWeekday && weekIndex == 0 ||
dayCounter > lastDayOfMonth
) {
// Empty cells
} else {
val date = selectedDate.withDayOfMonth(dayCounter)
val isSelected = date == selectedDate
Box(
modifier = Modifier
.size(40.dp)
.background(
color = if (isSelected) Color(0xFF6C63FF) else Color.Transparent,
shape = RoundedCornerShape(8.dp)
)
.clickable { onDateSelected(date) },
contentAlignment = Alignment.Center
) {
Text(
dayCounter.toString(),
color = if (isSelected) Color.White else Color.Black
)
}
dayCounter++
}
}
}
}
}
}
}
}
6. Performance оптимизации
LazyColumn с большим списком
@Composable
fun OptimizedLazyList(
items: List<Item>,
modifier: Modifier = Modifier
) {
LazyColumn(
modifier = modifier,
state = rememberLazyListState()
) {
items(
items = items,
key = { it.id }, // Важно для переиспользования
contentType = { it.type } // Оптимизация рендера
) { item ->
ItemRow(item = item)
}
}
}
// В ViewModel
class ListViewModel : ViewModel() {
private val _items = MutableStateFlow<List<Item>>(emptyList())
val items: StateFlow<List<Item>> = _items
.debounce(300) // Избежать частых обновлений
.distinctUntilChanged() // Пропустить дублирующиеся значения
.stateIn(viewModelScope, SharingStarted.Lazily, emptyList())
}
7. Theme система (Dark Mode)
@Composable
fun AppTheme(
darkTheme: Boolean = isSystemInDarkTheme(),
content: @Composable () -> Unit
) {
val colorScheme = when {
darkTheme -> darkColorScheme(
primary = Color(0xFF6C63FF),
secondary = Color(0xFF03DAC6),
background = Color(0xFF121212),
surface = Color(0xFF1E1E1E)
)
else -> lightColorScheme(
primary = Color(0xFF6C63FF),
secondary = Color(0xFF03DAC6),
background = Color.White,
surface = Color(0xFFF5F5F5)
)
}
MaterialTheme(
colorScheme = colorScheme,
typography = appTypography,
content = content
)
}
Основные техники, которые использую
- State Management — StateFlow, MutableState
- Navigation — Jetpack Navigation
- Animations — animateAsState, animateContentSize, transition
- Modifiers — custom modifiers, chain composition
- LazyLayout — LazyColumn, LazyRow, LazyGrid для больших списков
- Gestures — tap, scroll, swipe detection
- Shapes & Colors — Gradient, rounded corners, theme system
- Performance — remember, memoization, debounce
- Testing — Compose Test API для unit тестов
- Accessibility — semantics, contentDescription
Вывод
Я имею серьёзный опыт работы с Jetpack Compose от простых до очень сложных UI. Создал полнофункциональные приложения с анимациями, real-time обновлениями, офлайн синхронизацией и высокой производительностью.