Android UI Development
Jetpack Compose provides declarative, type-safe UI development for Android. Proper state management with remember, State, and StateFlow prevents UI bugs and enables predictable behavior. Navigation Compose enables deep linking for push notifications and external entry points. Reusable composables and Material Design 3 ensure consistency across screens and reduce maintenance burden.
Overview
This guide covers Jetpack Compose best practices for building robust, accessible user interfaces. We focus on declarative UI patterns, state management, navigation strategies, Material Design 3 theming, and reusable component design.
What This Guide Covers
- Jetpack Compose Basics: Declarative syntax, composables, modifiers
- State Management:
remember,State,StateFlow,collectAsStateWithLifecycle - Navigation: Navigation Compose, type-safe routes, deep linking
- Material Design 3: Theming, colors, typography, dynamic colors
- Reusable Components: Building modular, testable UI components
- Performance: Recomposition optimization, derivedStateOf, key
Compose State Management
State in Jetpack Compose represents any value that can change over time. When state changes, Compose automatically recomposes (re-executes) the affected composable functions to update the UI. This declarative approach eliminates manual view updates and reduces bugs.
Compose uses a unidirectional data flow pattern where state flows down from ViewModels to composables, and events flow up from user interactions back to ViewModels. This separation ensures predictable behavior and makes testing straightforward.
State in Compose
State Management Rules
Understanding when to use each state mechanism is critical for building performant, maintainable UIs. remember caches values across recompositions but loses state during configuration changes (like rotation). rememberSaveable persists state through these changes using the Bundle mechanism. StateFlow from ViewModels provides lifecycle-aware state that survives both recompositions and configuration changes, making it ideal for business data. derivedStateOf optimizes performance by only recomposing when computed values actually change, not when intermediate dependencies change.
| State Type | Use Case | Scope | Example |
|---|---|---|---|
remember | Local UI state | Composable | Toggle, text input |
mutableStateOf | Observable state | Composable | Selected item, expanded state |
StateFlow | ViewModel state | ViewModel | Data from repository |
derivedStateOf | Computed state | Composable | Filtered lists, calculations |
rememberSaveable | Survive config changes | Composable | Scroll position, form data |
Jetpack Compose Basics
Jetpack Compose uses a declarative paradigm where you describe what the UI should look like for a given state, rather than imperatively updating views. Composable functions are the building blocks - they take data and emit UI elements. The @Composable annotation marks functions that can call other composables and participate in recomposition.
The Scaffold composable provides Material Design layout structure with slots for common UI elements like top bars, floating action buttons, and bottom navigation. Using Scaffold ensures consistent layout behavior and handles insets automatically. The example below demonstrates proper state collection using collectAsStateWithLifecycle(), which respects the Android lifecycle and prevents memory leaks by stopping collection when the UI is not visible. See Android Architecture for ViewModel patterns and Android Data & Networking for repository integration.
Basic Screen with Material Design 3
// presentation/screens/payments/PaymentListScreen.kt
package com.bank.paymentapp.presentation.screens.payments
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.material3.*
import androidx.compose.runtime.*
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.dp
import androidx.hilt.navigation.compose.hiltViewModel
import androidx.lifecycle.compose.collectAsStateWithLifecycle
@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun PaymentListScreen(
viewModel: PaymentListViewModel = hiltViewModel(),
onPaymentClick: (String) -> Unit
) {
val state by viewModel.state.collectAsStateWithLifecycle()
Scaffold(
topBar = {
TopAppBar(
title = { Text("Payments") },
colors = TopAppBarDefaults.topAppBarColors(
containerColor = MaterialTheme.colorScheme.primary,
titleContentColor = MaterialTheme.colorScheme.onPrimary
)
)
},
floatingActionButton = {
FloatingActionButton(
onClick = { /* Navigate to create payment */ }
) {
Icon(Icons.Default.Add, contentDescription = "Create Payment")
}
}
) { paddingValues ->
when (state) {
is PaymentListState.Loading -> {
LoadingContent(modifier = Modifier.padding(paddingValues))
}
is PaymentListState.Success -> {
val payments = (state as PaymentListState.Success).payments
PaymentListContent(
payments = payments,
onPaymentClick = onPaymentClick,
modifier = Modifier.padding(paddingValues)
)
}
is PaymentListState.Error -> {
ErrorContent(
message = (state as PaymentListState.Error).message,
onRetry = { viewModel.loadPayments() },
modifier = Modifier.padding(paddingValues)
)
}
}
}
}
@Composable
private fun LoadingContent(modifier: Modifier = Modifier) {
Box(
modifier = modifier.fillMaxSize(),
contentAlignment = Alignment.Center
) {
CircularProgressIndicator()
}
}
@Composable
private fun PaymentListContent(
payments: List<Payment>,
onPaymentClick: (String) -> Unit,
modifier: Modifier = Modifier
) {
LazyColumn(
modifier = modifier.fillMaxSize(),
contentPadding = PaddingValues(16.dp),
verticalArrangement = Arrangement.spacedBy(12.dp)
) {
items(
items = payments,
key = { it.id }
) { payment ->
PaymentCard(
payment = payment,
onClick = { onPaymentClick(payment.id) }
)
}
}
}
Compose Lifecycle
Reusable Components
Building reusable composable components is fundamental to maintaining consistent UI and reducing code duplication. Well-designed components accept only the data they need via parameters and expose behavior through lambda callbacks. This makes them testable in isolation and reusable across different contexts.
The key principle is state hoisting - components should be stateless and receive their state from parents. This pattern, borrowed from React, makes components predictable and testable. Stateless components can be easily previewed with different states using @Preview annotations. For complex styling, Material Design 3 provides the Card, Surface, and Button composables with built-in elevation, shape, and color handling based on the theme.
Payment Card Component
// presentation/components/PaymentCard.kt
package com.bank.paymentapp.presentation.components
import androidx.compose.foundation.layout.*
import androidx.compose.material3.*
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.unit.dp
import com.bank.paymentapp.domain.model.Payment
import com.bank.paymentapp.domain.model.PaymentStatus
import java.text.NumberFormat
import java.text.SimpleDateFormat
import java.util.*
@Composable
fun PaymentCard(
payment: Payment,
onClick: () -> Unit,
modifier: Modifier = Modifier
) {
Card(
onClick = onClick,
modifier = modifier.fillMaxWidth(),
elevation = CardDefaults.cardElevation(defaultElevation = 2.dp)
) {
Column(
modifier = Modifier.padding(16.dp)
) {
// Header
Row(
modifier = Modifier.fillMaxWidth(),
horizontalArrangement = Arrangement.SpaceBetween,
verticalAlignment = Alignment.CenterVertically
) {
Text(
text = payment.recipientName,
style = MaterialTheme.typography.titleMedium,
fontWeight = FontWeight.Bold
)
PaymentStatusChip(status = payment.status)
}
Spacer(modifier = Modifier.height(8.dp))
// Amount
Text(
text = formatCurrency(payment.amount, payment.currency),
style = MaterialTheme.typography.headlineSmall,
color = MaterialTheme.colorScheme.primary
)
Spacer(modifier = Modifier.height(4.dp))
// Date
Text(
text = formatDate(payment.createdAt),
style = MaterialTheme.typography.bodySmall,
color = MaterialTheme.colorScheme.onSurfaceVariant
)
// Description (if exists)
payment.description?.let { description ->
Spacer(modifier = Modifier.height(8.dp))
Text(
text = description,
style = MaterialTheme.typography.bodyMedium,
maxLines = 2
)
}
}
}
}
@Composable
private fun PaymentStatusChip(status: PaymentStatus) {
val (text, containerColor, contentColor) = when (status) {
PaymentStatus.COMPLETED -> Triple(
"Completed",
MaterialTheme.colorScheme.primaryContainer,
MaterialTheme.colorScheme.onPrimaryContainer
)
PaymentStatus.PENDING -> Triple(
"Pending",
MaterialTheme.colorScheme.secondaryContainer,
MaterialTheme.colorScheme.onSecondaryContainer
)
PaymentStatus.FAILED -> Triple(
"Failed",
MaterialTheme.colorScheme.errorContainer,
MaterialTheme.colorScheme.onErrorContainer
)
}
Surface(
color = containerColor,
contentColor = contentColor,
shape = MaterialTheme.shapes.small
) {
Text(
text = text,
modifier = Modifier.padding(horizontal = 8.dp, vertical = 4.dp),
style = MaterialTheme.typography.labelSmall,
fontWeight = FontWeight.Medium
)
}
}
private fun formatCurrency(amount: Double, currency: String): String {
val formatter = NumberFormat.getCurrencyInstance()
formatter.currency = Currency.getInstance(currency)
return formatter.format(amount)
}
private fun formatDate(date: Date): String {
val formatter = SimpleDateFormat("MMM dd, yyyy HH:mm", Locale.getDefault())
return formatter.format(date)
}
Empty State Component
// presentation/components/EmptyStateView.kt
@Composable
fun EmptyStateView(
icon: ImageVector,
title: String,
message: String,
actionText: String? = null,
onAction: (() -> Unit)? = null,
modifier: Modifier = Modifier
) {
Column(
modifier = modifier
.fillMaxSize()
.padding(32.dp),
horizontalAlignment = Alignment.CenterHorizontally,
verticalArrangement = Arrangement.Center
) {
Icon(
imageVector = icon,
contentDescription = null,
modifier = Modifier.size(64.dp),
tint = MaterialTheme.colorScheme.onSurfaceVariant
)
Spacer(modifier = Modifier.height(16.dp))
Text(
text = title,
style = MaterialTheme.typography.titleLarge,
color = MaterialTheme.colorScheme.onSurface
)
Spacer(modifier = Modifier.height(8.dp))
Text(
text = message,
style = MaterialTheme.typography.bodyMedium,
color = MaterialTheme.colorScheme.onSurfaceVariant,
textAlign = TextAlign.Center
)
if (actionText != null && onAction != null) {
Spacer(modifier = Modifier.height(24.dp))
Button(onClick = onAction) {
Text(text = actionText)
}
}
}
}
List and Interaction Patterns
LazyColumn for List Virtualization
Jetpack Compose's LazyColumn renders only visible items for optimal performance.
// presentation/screens/payments/PaymentListScreen.kt
@Composable
fun PaymentListScreen(
viewModel: PaymentListViewModel = hiltViewModel(),
onPaymentClick: (String) -> Unit
) {
val state by viewModel.state.collectAsStateWithLifecycle()
val payments = (state as? PaymentListState.Success)?.payments ?: emptyList()
LazyColumn(
modifier = Modifier.fillMaxSize(),
contentPadding = PaddingValues(16.dp),
verticalArrangement = Arrangement.spacedBy(12.dp)
) {
items(
items = payments,
key = { it.id }
) { payment ->
PaymentCard(
payment = payment,
onClick = { onPaymentClick(payment.id) }
)
}
}
}
Why LazyColumn: Renders only visible items, recycling composables as you scroll. Essential for lists with more than 20-30 items to prevent memory issues and maintain smooth scrolling.
Pull-to-Refresh
Material 3 provides PullRefreshIndicator for pull-to-refresh functionality:
import androidx.compose.material.ExperimentalMaterialApi
import androidx.compose.material.pullrefresh.PullRefreshIndicator
import androidx.compose.material.pullrefresh.pullRefresh
import androidx.compose.material.pullrefresh.rememberPullRefreshState
@OptIn(ExperimentalMaterialApi::class)
@Composable
fun TransactionListScreen(
viewModel: TransactionViewModel = hiltViewModel()
) {
val isRefreshing by viewModel.isRefreshing.collectAsState()
val transactions by viewModel.transactions.collectAsState()
val pullRefreshState = rememberPullRefreshState(
refreshing = isRefreshing,
onRefresh = { viewModel.refresh() }
)
Box(
modifier = Modifier
.fillMaxSize()
.pullRefresh(pullRefreshState)
) {
LazyColumn {
items(transactions) { transaction ->
TransactionRow(transaction = transaction)
}
}
PullRefreshIndicator(
refreshing = isRefreshing,
state = pullRefreshState,
modifier = Modifier.align(Alignment.TopCenter)
)
}
}
Gesture Handling
Jetpack Compose provides gesture detection through Modifier.pointerInput:
@Composable
fun DraggableCard() {
var offsetX by remember { mutableStateOf(0f) }
var offsetY by remember { mutableStateOf(0f) }
Box(
modifier = Modifier
.size(200.dp, 100.dp)
.offset { IntOffset(offsetX.roundToInt(), offsetY.roundToInt()) }
.background(Color.Blue, RoundedCornerShape(12.dp))
.pointerInput(Unit) {
detectDragGestures(
onDragEnd = {
// Animate back to original position
offsetX = 0f
offsetY = 0f
}
) { change, dragAmount ->
change.consume()
offsetX += dragAmount.x
offsetY += dragAmount.y
}
}
)
}
Common Gestures:
detectTapGestures: Tap, double-tap, long pressdetectDragGestures: Pan/dragdetectTransformGestures: Pinch, zoom, rotateModifier.clickable: Simple click handling
// Example: Swipe-to-dismiss
import androidx.compose.material3.SwipeToDismiss
import androidx.compose.material3.rememberDismissState
@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun SwipeableListItem(
item: Item,
onDismiss: () -> Unit
) {
val dismissState = rememberDismissState(
confirmValueChange = {
if (it == DismissValue.DismissedToStart) {
onDismiss()
true
} else {
false
}
}
)
SwipeToDismiss(
state = dismissState,
background = {
Box(
modifier = Modifier
.fillMaxSize()
.background(Color.Red),
contentAlignment = Alignment.CenterEnd
) {
Icon(
Icons.Default.Delete,
contentDescription = "Delete",
modifier = Modifier.padding(16.dp)
)
}
}
) {
Card(modifier = Modifier.fillMaxWidth()) {
Text(
text = item.name,
modifier = Modifier.padding(16.dp)
)
}
}
}
Navigation
For foundational navigation concepts (stack, tab, drawer, modal patterns), see Mobile Navigation. This section covers Jetpack Compose-specific navigation implementation with Navigation Component.
Navigation in Jetpack Compose is handled by the Navigation Component, which manages the back stack and handles deep links. Unlike traditional Fragment-based navigation, Navigation Compose uses a NavHost that displays different composables based on the current route. The NavController manages navigation operations like navigate, popBackStack, and handles the back stack state.
Type-safe navigation is achieved using sealed classes to represent screens, preventing runtime errors from typos in route strings. Arguments are passed through the route URL or as navigation arguments, with type safety enforced through navArgument definitions. This approach integrates seamlessly with deep links from push notifications or external URLs. For complete navigation patterns including bottom navigation and nested graphs, see the Navigation Compose documentation.
Navigation Architecture
Navigation Setup
// presentation/navigation/NavGraph.kt
package com.bank.paymentapp.presentation.navigation
import androidx.compose.runtime.Composable
import androidx.navigation.NavHostController
import androidx.navigation.compose.NavHost
import androidx.navigation.compose.composable
import androidx.navigation.navArgument
import androidx.navigation.NavType
import com.bank.paymentapp.presentation.screens.payments.PaymentListScreen
import com.bank.paymentapp.presentation.screens.payments.PaymentDetailScreen
import com.bank.paymentapp.presentation.screens.auth.LoginScreen
@Composable
fun NavGraph(
navController: NavHostController,
startDestination: String = Screen.PaymentList.route
) {
NavHost(
navController = navController,
startDestination = startDestination
) {
composable(Screen.Login.route) {
LoginScreen(
onLoginSuccess = {
navController.navigate(Screen.PaymentList.route) {
popUpTo(Screen.Login.route) { inclusive = true }
}
}
)
}
composable(Screen.PaymentList.route) {
PaymentListScreen(
onPaymentClick = { paymentId ->
navController.navigate(Screen.PaymentDetail.createRoute(paymentId))
}
)
}
composable(
route = Screen.PaymentDetail.route,
arguments = listOf(
navArgument("paymentId") { type = NavType.StringType }
)
) { backStackEntry ->
val paymentId = backStackEntry.arguments?.getString("paymentId")
PaymentDetailScreen(
paymentId = paymentId ?: "",
onBack = { navController.popBackStack() }
)
}
}
}
sealed class Screen(val route: String) {
data object Login : Screen("login")
data object PaymentList : Screen("payments")
data object PaymentDetail : Screen("payment/{paymentId}") {
fun createRoute(paymentId: String) = "payment/$paymentId"
}
data object CreatePayment : Screen("payment/create")
data object Profile : Screen("profile")
}
Navigation Flow
Material Design 3 Theming
Material Design 3 (Material You) introduces dynamic theming based on the user's wallpaper and system preferences. Themes define color schemes, typography, and shapes used throughout the app. The MaterialTheme composable provides these values to all child composables through composition locals.
Color schemes consist of primary, secondary, tertiary colors plus their variants (container, on-container). The "on" prefix indicates colors used for text/icons displayed on that color surface. Dynamic colors, available on Android 12+, adapt to the user's wallpaper. For branding requirements, use custom color schemes but follow Material Design accessibility guidelines with minimum 4.5:1 contrast ratios for text. Typography uses scaled text sizes that respect user accessibility settings. For comprehensive theming guidance, see Material Design 3.
Theme Setup
// presentation/theme/Theme.kt
package com.bank.paymentapp.presentation.theme
import android.app.Activity
import android.os.Build
import androidx.compose.foundation.isSystemInDarkTheme
import androidx.compose.material3.*
import androidx.compose.runtime.Composable
import androidx.compose.runtime.SideEffect
import androidx.compose.ui.graphics.toArgb
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.platform.LocalView
import androidx.core.view.WindowCompat
private val LightColorScheme = lightColorScheme(
primary = BankPrimary,
onPrimary = BankOnPrimary,
primaryContainer = BankPrimaryContainer,
onPrimaryContainer = BankOnPrimaryContainer,
secondary = BankSecondary,
onSecondary = BankOnSecondary,
tertiary = BankTertiary,
error = BankError,
onError = BankOnError,
background = BankBackground,
onBackground = BankOnBackground,
surface = BankSurface,
onSurface = BankOnSurface
)
private val DarkColorScheme = darkColorScheme(
primary = BankPrimaryDark,
onPrimary = BankOnPrimaryDark,
primaryContainer = BankPrimaryContainerDark,
onPrimaryContainer = BankOnPrimaryContainerDark,
secondary = BankSecondaryDark,
onSecondary = BankOnSecondaryDark,
tertiary = BankTertiaryDark,
error = BankErrorDark,
background = BankBackgroundDark,
surface = BankSurfaceDark
)
@Composable
fun BankingAppTheme(
darkTheme: Boolean = isSystemInDarkTheme(),
dynamicColor: Boolean = false,
content: @Composable () -> Unit
) {
val colorScheme = when {
dynamicColor && Build.VERSION.SDK_INT >= Build.VERSION_CODES.S -> {
val context = LocalContext.current
if (darkTheme) dynamicDarkColorScheme(context) else dynamicLightColorScheme(context)
}
darkTheme -> DarkColorScheme
else -> LightColorScheme
}
val view = LocalView.current
if (!view.isInEditMode) {
SideEffect {
val window = (view.context as Activity).window
window.statusBarColor = colorScheme.primary.toArgb()
WindowCompat.getInsetsController(window, view).isAppearanceLightStatusBars = !darkTheme
}
}
MaterialTheme(
colorScheme = colorScheme,
typography = BankingTypography,
content = content
)
}
Design System Colors
// presentation/theme/Color.kt
import androidx.compose.ui.graphics.Color
// Light theme colors
val BankPrimary = Color(0xFF0066CC)
val BankOnPrimary = Color(0xFFFFFFFF)
val BankPrimaryContainer = Color(0xFFD1E4FF)
val BankOnPrimaryContainer = Color(0xFF001D35)
val BankSecondary = Color(0xFF535F70)
val BankOnSecondary = Color(0xFFFFFFFF)
val BankTertiary = Color(0xFF6B5778)
val BankError = Color(0xFFBA1A1A)
val BankOnError = Color(0xFFFFFFFF)
val BankBackground = Color(0xFFFDFCFF)
val BankOnBackground = Color(0xFF1A1C1E)
val BankSurface = Color(0xFFFDFCFF)
val BankOnSurface = Color(0xFF1A1C1E)
// Dark theme colors
val BankPrimaryDark = Color(0xFF9ECAFF)
val BankOnPrimaryDark = Color(0xFF003258)
// ... additional dark colors
Performance Optimization
Compose's recomposition system is intelligent but requires proper optimization for complex UIs. Recomposition occurs when state changes, but Compose's smart recomposition only re-executes composables that read the changed state. If a composable doesn't read a changed state value, it skips recomposition entirely. This granular recomposition is what makes Compose performant, but requires thoughtful state structure to maximize efficiency.
The primary optimization techniques involve making state changes granular (use multiple State objects instead of one large object), using derivedStateOf for computed values (prevents recomposition when intermediate calculations don't change the result), and providing stable keys for LazyColumn items (enables efficient item reuse). Using @Immutable or @Stable annotations helps Compose understand when objects haven't changed, preventing unnecessary recompositions. For deep-dive performance optimization including profiling recompositions, see Android Performance.
Recomposition Optimization
Common Mistakes
Compose's declarative, reactive paradigm differs from the imperative View system. Developers transitioning from XML layouts and findViewById often make the same mistakes initially. Recognizing these anti-patterns helps you avoid performance issues and subtle bugs.
Don't: Create New Lambdas in Composables
// BAD: New lambda on every recomposition causes child recompositions
@Composable
fun PaymentList(payments: List<Payment>) {
LazyColumn {
items(payments) { payment ->
PaymentCard(
payment = payment,
onClick = { handleClick(payment.id) } // New lambda every time!
)
}
}
}
// GOOD: Stable lambda or remember
@Composable
fun PaymentList(
payments: List<Payment>,
onPaymentClick: (String) -> Unit
) {
LazyColumn {
items(
items = payments,
key = { it.id }
) { payment ->
PaymentCard(
payment = payment,
onClick = { onPaymentClick(payment.id) }
)
}
}
}
Don't: Use mutableStateOf Without remember
// BAD: State resets on every recomposition
@Composable
fun PaymentFilter() {
var expanded = mutableStateOf(false) // WRONG!
Button(onClick = { expanded.value = !expanded.value }) {
Text("Filter")
}
}
// GOOD: Use remember
@Composable
fun PaymentFilter() {
var expanded by remember { mutableStateOf(false) }
Button(onClick = { expanded = !expanded }) {
Text("Filter")
}
}
Code Review Checklist
Security (Block PR)
- No sensitive data displayed in plain text - Mask account numbers
- Screenshot protection on sensitive screens - Use FLAG_SECURE
- Biometric auth before sensitive actions - Payments require authentication
Watch For (Request Changes)
- Stable keys in LazyColumn - Use unique IDs, not index
- No heavy operations in composables - Use LaunchedEffect/rememberCoroutineScope
- State hoisting - Stateless composables where possible
- Content descriptions - All interactive elements accessible
- Remember expensive calculations - Use remember or derivedStateOf
- Proper navigation - Use NavController, not manual Fragment management
Best Practices
- @Composable functions - Start with capital letter
- Modifier parameter last - Standard Compose convention
- Preview annotations - All composables have @Preview
- Material Design 3 - Use MaterialTheme colors/typography
- Hilt ViewModel injection - Use hiltViewModel()
- collectAsStateWithLifecycle - Collect StateFlow lifecycle-aware
Further Reading
Android Framework Guidelines
- Android Overview - Project setup and fundamentals
- Android Data & Networking - ViewModels consuming repository data
- Android Security - Secure UI practices and screenshot protection
- Android Architecture - MVVM presentation layer with Compose
- Android Testing - Testing composables and UI components
- Android Performance - Compose performance optimization
UI and Mobile Guidelines
- Mobile Navigation - Universal mobile navigation patterns and concepts
- Mobile Overview - Cross-platform mobile UI patterns
- Web State Management - Universal state management patterns and principles
- Web Accessibility - Accessibility principles (applicable to mobile)
- Web Components - Component design patterns
External Resources
- Jetpack Compose Documentation
- Material Design 3
- Compose State
- Compose Navigation
- Compose Performance
Summary
Key Takeaways
- Declarative UI - Compose functions describe UI state
- State management - Use proper state holders (remember, StateFlow)
- Recomposition - Only affected composables recompose
- Navigation Compose - Type-safe navigation with deep linking
- Material Design 3 - Consistent theming and design system
- Reusable composables - Build modular, testable components
- Performance - Use keys, derivedStateOf, stable types
- Accessibility - Content descriptions, semantic properties
- Previews - Every composable has @Preview
- State hoisting - Keep composables stateless when possible
Next Steps: Explore Android Data & Networking for integrating UI with backend APIs and local storage.