← Назад к вопросам

Можешь рассказать что верстал на 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
    )
}

Основные техники, которые использую

  1. State Management — StateFlow, MutableState
  2. Navigation — Jetpack Navigation
  3. Animations — animateAsState, animateContentSize, transition
  4. Modifiers — custom modifiers, chain composition
  5. LazyLayout — LazyColumn, LazyRow, LazyGrid для больших списков
  6. Gestures — tap, scroll, swipe detection
  7. Shapes & Colors — Gradient, rounded corners, theme system
  8. Performance — remember, memoization, debounce
  9. Testing — Compose Test API для unit тестов
  10. Accessibility — semantics, contentDescription

Вывод

Я имею серьёзный опыт работы с Jetpack Compose от простых до очень сложных UI. Создал полнофункциональные приложения с анимациями, real-time обновлениями, офлайн синхронизацией и высокой производительностью.