Imperative vs Declarative UI
The Problem with XML Layouts
With traditional Android development, you define UI in XML and imperatively mutate it from Kotlin/Java code. This creates a split between what the UI looks like (XML) and how it changes (code). As screens grow complex, synchronizing these two worlds becomes error-prone.
<TextView
android:id="@+id/greeting"
android:text="Hello"
android:layout_width="wrap_content"
android:layout_height="wrap_content" />
findViewById<TextView>(R.id.greeting).text = "Hello, $name"
Every time state changes, you manually find views and update them. Forgetting one update causes bugs that are hard to trace.
The Declarative Alternative
Jetpack Compose flips this model. You write one function that describes what the UI should look like given a set of inputs. When inputs change, Compose automatically re-runs the function and updates the screen.
@Composable
fun Greeting(name: String) {
Text(text = "Hello, $name")
}
There is no XML. There is no findViewById. You describe the desired state, and Compose figures out the diff.
How Compose Works Under the Hood
Compose uses a compiler plugin that transforms your @Composable functions into instructions for a slot table -- a data structure that tracks what UI nodes exist and where. When state changes, Compose marks affected composables as invalid and re-executes only those functions. The runtime compares the new slot table against the previous one and issues targeted updates to the Android View system.
This process is called recomposition. It is not the same as rebuilding the entire view tree -- Compose is smart enough to skip recomposition of composables whose inputs have not changed.
Side-by-Side Comparison
| Aspect | XML Layouts | Compose |
|---|---|---|
| UI Definition | XML files | Kotlin functions |
| State Updates | Manual setText, setVisibility | Automatic via state changes |
| Preview | Layout Editor | @Preview annotation |
| Lists | RecyclerView + ViewHolder | LazyColumn built-in |
| Navigation | NavController + XML graphs | NavHost composable |
| Testing | Espresso | Compose testing (semantics) |
When to Use Compose
Compose is the recommended approach for new Android projects. Google has committed to Compose as the future of Android UI. You will still encounter XML in legacy codebases. The two can coexist via ComposeView and AndroidView.
The Compose Runtime and Recomposition
What Happens When You Call a Composable
When setContent is called, Compose builds the initial composition by executing your composable functions. Each function call produces UI nodes in the slot table. This first pass is called composition.
After composition, the UI is rendered. When something changes -- a button tap, a data fetch completing -- Compose identifies which composables read that changed value and re-executes only those. This is recomposition.
Recomposition Is Not Rebuild
Recomposition does not re-execute every composable in the tree. The compiler performs several optimizations:
- Smart skipping: Composables whose parameters have not changed are skipped.
- Restartable points: Each composable is a restartable point, allowing fine-grained recomposition.
- Inline lambdas: Layout lambdas are inlined, avoiding allocation overhead.
@Composable
fun UserProfile(name: String, avatar: ImageBitmap) {
Column {
Image(bitmap = avatar, contentDescription = null)
Text(text = name)
}
}
Stability and Skippability
For Compose to skip recomposition, parameters must be stable. A stable type is one whose composition is guaranteed not to change between recompositions. Primitive types, String, and Kotlin data classes with stable properties are stable. Classes that hold var properties or mutable collections are not.
// Stable - data class with val properties
data class User(val name: String, val age: Int)
// Unstable - holds mutable state
class MutableUser {
var name: String = ""
}
You can check stability with the @Stable or @Immutable annotations, or inspect the build output with composeCompilerReports.
Composition Lifecycle
- Initial composition: Build the entire UI tree from scratch.
- Recomposition: Re-execute invalid composables to update the tree.
- Disposal: Remove UI nodes that are no longer in the tree.
Understanding this lifecycle is critical for avoiding bugs with side effects.
Performance Implications
Recomposition is fast because Compose only re-executes the smallest possible scope. However, unnecessary recompositions can still hurt performance. Common causes:
- Passing unstable types as parameters
- Creating new lambda instances on every recomposition
- Performing expensive calculations inside composable functions without derivedStateOf or remember
Building Your First Compose App
Anatomy of a Compose Screen
A typical Compose screen is a single composable function that describes the full layout. It takes state as input and returns UI as output.
@Composable
fun TodoScreen(tasks: List<TodoItem>) {
Scaffold(
topBar = {
TopAppBar(title = { Text("My Tasks") })
}
) { padding ->
LazyColumn(contentPadding = padding) {
items(tasks) { task ->
TodoItemRow(task)
}
}
}
}
No fragments. No adapters. No XML. Just functions composed together.
The @Preview Annotation
Compose provides @Preview to render composables in Android Studio without running the app:
@Preview(showBackground = true)
@Composable
fun GreetingPreview() {
MaterialTheme {
Greeting("Preview")
}
}
You can preview multiple configurations:
@Preview(name = "Light Mode", showBackground = true)
@Preview(name = "Dark Mode", uiMode = Configuration.UI_MODE_NIGHT_YES)
@Composable
fun ThemePreview() {
MaterialTheme {
Greeting("Theme Test")
}
}
Composable Functions Are Regular Functions
Despite the @Composable annotation, these are just Kotlin functions. They can be called from other composables, passed as parameters, and returned from higher-order functions. The annotation signals the Compose compiler to apply special transformations.
@Composable
fun Header() {
Text("Welcome")
}
@Composable
fun Screen() {
Column {
Header()
Text("Content below")
}
}
Quiz
1. What is the primary difference between XML layouts and Compose?
2. What is recomposition in Jetpack Compose?
3. Which annotation marks a composable for Android Studio preview?
4. Why might a composable fail to be skipped during recomposition?
Flashcards
Question
What does declarative UI mean in Compose?
Click to reveal answer
Answer
You describe what the UI should look like for a given state, rather than imperatively mutating views when state changes.
Question
What is the slot table in Compose?
Click to reveal answer
Answer
A data structure generated by the Compose compiler that tracks UI nodes, their positions, and their values -- enabling efficient diffing and targeted updates.
Question
What annotation is required for a function to use Compose APIs?
Click to reveal answer
Answer
@Composable -- it signals the Compose compiler to apply special transformations to the function.
Question
How does Compose handle list rendering compared to RecyclerView?
Click to reveal answer
Answer
LazyColumn and LazyRow provide built-in lazy list behavior without needing adapters, ViewHolders, or XML layouts.
Revision Notes
Key Takeaways
- 1. Compose replaces XML layouts with Kotlin functions -- no findViewById, no adapters
- 2. Recomposition is fine-grained: only composable functions with changed inputs re-execute
- 3. Stable types enable Compose to skip unnecessary recompositions
- 4. Compose can coexist with XML views via ComposeView and AndroidView
Interview Tips
- • Be ready to explain the difference between composition and recomposition
- • Know why stability matters -- interviewers often ask about recomposition performance
- • Understand the Compose runtime lifecycle: initial composition, recomposition, disposal
- • Compare Compose to other declarative frameworks (SwiftUI, Flutter) for system design discussions
Cheat Sheet
Compose Introduction Cheat Sheet
Paradigm Shift:
- XML = imperative (find view, mutate view)
- Compose = declarative (call function with state, UI updates automatically)
Key Concepts:
- Composition: initial build of the UI tree
- Recomposition: re-execution of composables with changed inputs
- Stability: types must be stable for recomposition skipping
Setup:
- buildFeatures.compose = true in build.gradle.kts
- Use Compose BOM for version management
- setContent { } in Activity to set the composable root
Preview:
- @Preview(showBackground = true) for Android Studio preview
- Multiple @Preview annotations for different configurations