Mastering Jetpack Compose Foundations
October 1, 2025 · 6 min read
Android, Kotlin, Jetpack Compose, Mobile Development, Interview Prep
Part 1 of the Android Deep Dive series
Jetpack Compose has become the de facto standard for Android UI development. In 2025-2026, virtually every Android interview expects deep Compose knowledge. With KMP (Kotlin Multiplatform) reaching enterprise readiness and Compose Multiplatform gaining momentum, understanding Compose fundamentals is more critical than ever.
Why Compose Matters in 2025-2026
According to recent developer surveys, Compose adoption has exploded. Google has made Compose the default for modern Android development, with major libraries like Room, DataStore, and ViewModel now offering KMP support. Interviewers expect candidates to demonstrate not just familiarity, but mastery of declarative UI patterns.
Industry Trend
Companies report reducing UI code by 30-50% after migrating to Compose. Interviewers often ask about migration strategies and when XML views are still appropriate.
Core Concepts
Composable Functions
Composables are functions annotated with @Composable that describe UI. They emit UI elements rather than returning values.
@Composable
fun NutritionCard(
title: String,
calories: Int,
modifier: Modifier = Modifier
) {
Card(
modifier = modifier.fillMaxWidth(),
colors = CardDefaults.cardColors(
containerColor = MaterialTheme.colorScheme.surfaceVariant
)
) {
Column(
modifier = Modifier.padding(16.dp),
verticalArrangement = Arrangement.spacedBy(8.dp)
) {
Text(
text = title,
style = MaterialTheme.typography.headlineSmall
)
Text(
text = "$calories kcal",
style = MaterialTheme.typography.bodyMedium,
color = MaterialTheme.colorScheme.onSurfaceVariant
)
}
}
}
Modifiers: Order Matters
Modifiers configure size, layout, behavior, and appearance. They're applied as a chain wrapping the content - order is critical.
// Padding then background: transparent padding, red content
Box(
modifier = Modifier
.padding(16.dp)
.background(Color.Red)
)
// Background then padding: red extends under padding
Box(
modifier = Modifier
.background(Color.Red)
.padding(16.dp)
)
// These produce DIFFERENT results!
Interview Tip
Interviewers love asking about modifier order. Think of it as nested boxes - each modifier wraps what came before.
Common Modifier Patterns
// Size modifiers
Modifier
.size(100.dp) // Fixed size
.fillMaxWidth() // Full parent width
.fillMaxSize() // Full parent size
.wrapContentSize() // Wrap content
// Spacing
Modifier
.padding(16.dp) // All sides
.padding(horizontal = 8.dp, vertical = 16.dp)
// Appearance
Modifier
.background(Color.Blue, RoundedCornerShape(8.dp))
.clip(CircleShape)
.border(1.dp, Color.Gray, RoundedCornerShape(8.dp))
// Interaction
Modifier
.clickable { /* handle click */ }
.weight(1f) // For Row/Column children
Layout Composables
// Column - Vertical arrangement
@Composable
fun VerticalMealList() {
Column(
modifier = Modifier.fillMaxWidth(),
verticalArrangement = Arrangement.spacedBy(8.dp),
horizontalAlignment = Alignment.CenterHorizontally
) {
Text("Breakfast")
Text("Lunch")
Text("Dinner")
}
}
// Row - Horizontal arrangement
@Composable
fun MacroRow() {
Row(
modifier = Modifier.fillMaxWidth(),
horizontalArrangement = Arrangement.SpaceBetween,
verticalAlignment = Alignment.CenterVertically
) {
Text("Calories")
Spacer(modifier = Modifier.weight(1f))
Text("1,200 kcal")
}
}
// Box - Overlapping content
@Composable
fun ImageWithBadge() {
Box(contentAlignment = Alignment.Center) {
Image(/* meal photo */)
Badge(modifier = Modifier.align(Alignment.TopEnd)) {
Text("New")
}
}
}
Lists with LazyColumn
@Composable
fun MealList(meals: List<Meal>) {
LazyColumn(
modifier = Modifier.fillMaxSize(),
contentPadding = PaddingValues(16.dp),
verticalArrangement = Arrangement.spacedBy(8.dp)
) {
items(
items = meals,
key = { meal -> meal.id } // Stable keys for performance
) { meal ->
MealCard(meal = meal)
}
}
}
Common Mistake
Always provide stable keys to list items. Using index as key causes issues with reordering and animations.
Material Design 3 Integration
@Composable
fun AppTheme(
darkTheme: Boolean = isSystemInDarkTheme(),
dynamicColor: Boolean = true,
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()
}
MaterialTheme(
colorScheme = colorScheme,
typography = Typography,
content = content
)
}
Interview Questions
Q: What is recomposition and how does Compose optimize it?
Recomposition is Compose's process of re-executing composable functions when state changes. Compose optimizes through:
- Skip unchanged composables - If inputs haven't changed, skip recomposition
- Positional memoization - Compose tracks composables by position in the tree
- Smart recomposition - Only affected parts recompose, not the entire tree
@Composable
fun ParentComposable() {
var count by remember { mutableStateOf(0) }
Column {
Button(onClick = { count++ }) {
Text("Count: $count") // Recomposes when count changes
}
StaticChild() // Does NOT recompose - no dependency on count
}
}
Q: Explain the difference between remember and rememberSaveable.
remember- Survives recomposition, but NOT configuration changes (rotation)rememberSaveable- Survives both recomposition AND configuration changes
var count by remember { mutableStateOf(0) } // Lost on rotation
var count by rememberSaveable { mutableStateOf(0) } // Preserved on rotation
Q: How do you create a custom Composable that accepts standard modifiers?
Accept Modifier as parameter with default value, apply it first:
@Composable
fun MyCustomCard(
title: String,
modifier: Modifier = Modifier // Default empty modifier
) {
Card(
modifier = modifier // Apply caller's modifier
.fillMaxWidth() // Then component's own modifiers
) {
Text(title, modifier = Modifier.padding(16.dp))
}
}
Common Mistakes
1. Forgetting Keys in Lists
// BAD - No keys, causes issues with reordering/deletion
LazyColumn {
items(meals) { meal ->
MealCard(meal)
}
}
// GOOD - Stable keys
LazyColumn {
items(meals, key = { it.id }) { meal ->
MealCard(meal)
}
}
2. Heavy Computation in Composable
// BAD - Runs on every recomposition
@Composable
fun ExpensiveComposable(items: List<Item>) {
val sorted = items.sortedBy { it.date } // Expensive!
LazyColumn {
items(sorted) { ... }
}
}
// GOOD - Remember the computation
@Composable
fun OptimizedComposable(items: List<Item>) {
val sorted = remember(items) {
items.sortedBy { it.date }
}
LazyColumn {
items(sorted) { ... }
}
}
3. Not Using weight() in Row/Column
// BAD - Fixed sizes may not fill space
Row {
Text("Label")
Spacer(Modifier.width(8.dp))
Text("Value") // Won't push to end
}
// GOOD - weight distributes space
Row {
Text("Label")
Spacer(Modifier.weight(1f)) // Takes remaining space
Text("Value") // Pushed to end
}
Summary Table
| Concept | Purpose | Key Points |
|---|---|---|
@Composable | Declare UI functions | Cannot return values other than Unit |
Modifier | Configure appearance/behavior | Order matters (chain wrapping) |
remember | Cache across recomposition | Lost on config change |
rememberSaveable | Cache across config changes | Use for user input |
LazyColumn | Efficient scrolling lists | Always provide stable keys |
MaterialTheme | Consistent theming | Dynamic colors on Android 12+ |
Next in series: Part 2 - State Management covers ViewModel, StateFlow, and reactive patterns.