Jetpack Compose: Core Concepts Refresher
A spaced-repetition refresher on Core Concepts in Jetpack Compose, focused on practical implementation details and updates.
Table of contents
Context and Scope
This refresher distills nine Core Concepts of Jetpack Compose that every Android engineer should keep “front-of-mind” when reading, writing, or reviewing Compose UI. It assumes you’ve built a few screens already and want a precise, up-to-date mental model and production guidance as of March 16, 2026.
Covered in detail here (exactly nine):
- @Composable (must know)
- Declarative UI (must know)
- Composition Tree
- Recomposition (must know)
- Idempotency
- Composable Lifecycle
- Positional Memoization
- Pure Composables
- Slot API
Where relevant, notes reflect current guidance and recent updates in 2025–2026 (compiler/runtime, strong skipping, state lifespans). Citations appear inline. Official docs are prioritized.
Conceptual Model & Analogy
Think of Compose as a conductor leading an orchestra:
- The score is your declarative UI code: you describe the music (what the screen should look like) for the current “state.”
- The orchestra is the runtime: it builds and keeps an internal seating chart (the composition tree) of all players.
- When a musician’s sheet changes (state changes), the conductor doesn’t restart the concert; they cue only the affected sections to replay their parts (recomposition), while others keep playing.
- Each musician’s chair is tied to its seat, not their name (positional memoization). Move them without telling the conductor which musician moved (no keys), and chaos ensues.
- Great conductors demand repeatable rehearsals: if you replay the same bar with the same notes, it must sound the same (idempotency) and no one should run off-stage to call a server mid-bar (pure composables).
Deep Dive
- @Composable: “this function draws UI”
- The @Composable annotation tells the Kotlin compiler to transform a function so it can participate in composition, track reads of state, and be scheduled for recomposition. You cannot call composables from non-composable contexts except via entry points like setContent. (developer.android.com)
- Declarative UI
- In Compose, you describe what the UI should be for a given state; the runtime figures out the minimal updates needed. You re-call composables with new data; Compose handles updating on your behalf. This is the core “thinking in Compose” shift away from mutating views imperatively. (developer.android.com)
- Composition Tree
- When composables run, the runtime builds and maintains an internal tree representing your UI. This structure is updated across the three phases: composition (execute composables and build the tree), layout, and drawing. You rarely touch this directly, but understanding it helps reason about identity, keys, and effects. (developer.android.com)
- Recomposition
- When observed state or inputs change, Compose selectively re-executes only the affected parts of the tree. Many composables can be fully skipped if their inputs have not changed—skipping is a core optimization. Modern compiler features (strong skipping) increase how often safe skipping occurs. (developer.android.com)
- Idempotency
- Re-running a composable with the same inputs must yield the same UI result. This predictability enables safe skipping and re-entry at any time. Work that changes the outside world must not live in normal composable code paths; that belongs in effect handlers. (developer.android.com)
- Composable Lifecycle
- Each instance goes through: entering the composition, staying (0+ recompositions, sometimes skipped), and leaving the composition. Effects like LaunchedEffect/DisposableEffect tie setup/teardown to these boundaries. Identity matters: if you reorder without stable keys, Compose may dispose and recreate instead of moving. (developer.android.com)
- Positional Memoization
- remember stores values “by position” in the composition. If the call site moves, remembered values are forgotten unless you provide stable identity (key) or use movable content. Newer guidance clarifies lifespans across recompositions vs. Activity recreation (remember vs. rememberSaveable vs. rememberSerializable). (developer.android.com)
- Pure Composables
- A pure composable renders from inputs and local state without performing side effects (I/O, logging, toasts). Side effects exist, but should be explicit and scoped to effect APIs so re-execution/skip scheduling stays correct. (developer.android.com)
- Slot API
- Instead of passing dozens of configuration flags, many components accept composable lambdas (“slots”) that you fill with arbitrary UI (e.g., Scaffold’s topBar, floatingActionButton). This yields flexible, type-safe composition and testability. (developer.android.com)
Implementation Patterns
Baseline example: a tiny, pure, idempotent composable
@Composable
fun CounterCard(
title: String,
modifier: Modifier = Modifier
) {
// UI state local to this composable; survives recomposition but not process-death
var count by remember { mutableStateOf(0) } // positional memoization
Column(modifier.padding(16.dp)) {
Text(text = title, style = MaterialTheme.typography.titleMedium)
Spacer(Modifier.height(8.dp))
Row(verticalAlignment = Alignment.CenterVertically) {
Button(onClick = { count-- }) { Text("–") }
Text(
text = "$count",
modifier = Modifier.padding(horizontal = 16.dp),
style = MaterialTheme.typography.headlineSmall
)
Button(onClick = { count++ }) { Text("+") }
}
}
}
Why this is “good Compose”:
- @Composable marks a declarative function that emits UI nodes.
- All effects are UI-local; clicks mutate state that Compose observes, triggering recomposition only for the affected subtree. (developer.android.com)
- Running it twice with the same inputs returns the same structure (idempotent)—minus local state that’s appropriately encapsulated. If user-driven state must survive process death, use rememberSaveable or rememberSerializable instead. (developer.android.com)
Production-grade/advanced example: slot APIs, identity, lifespans, and effects
@Immutable
data class Message(
val id: String,
val author: String,
val text: String,
val timestamp: Long
)
@Composable
fun MessagesScreen(
messages: List<Message>,
onMessageClick: (Message) -> Unit,
// Slot API: inject top bar / FAB / content decorations from the host
topBar: @Composable () -> Unit = {},
floatingActionButton: @Composable () -> Unit = {}
) {
// Survive configuration changes and process death for scroll position, etc.
val listState = rememberSaveable(saver = LazyListState.Saver) {
LazyListState()
}
// Derived calculation memoized across recompositions while inputs are unchanged
val newestFirst by remember(messages) {
derivedStateOf { messages.sortedByDescending { it.timestamp } }
}
// Lifecycle-aware effect: start/stop a listener when this screen is in composition
DisposableEffect(Unit) {
// register something lightweight (e.g., analytics screen view)
onDispose {
// unregister/cleanup
}
}
Scaffold(
topBar = topBar,
floatingActionButton = floatingActionButton
) { padding ->
LazyColumn(
state = listState,
contentPadding = padding
) {
// Provide stable identity to preserve item instances when order changes
items(
items = newestFirst,
key = { it.id }, // identity to avoid unnecessary disposal/recreation
contentType = { "message" }
) { msg ->
// Keep this pure: render from inputs, emit events via lambdas
MessageRow(
message = msg,
onClick = { onMessageClick(msg) }
)
}
}
}
}
@Composable
private fun MessageRow(
message: Message,
onClick: () -> Unit
) {
// Skipping-friendly inputs: stable data + stable lambda reference
Row(
modifier = Modifier
.fillMaxWidth()
.clickable(onClick = onClick) // onClick is a parameter; compiler can memoize
.padding(16.dp)
) {
Column(Modifier.weight(1f)) {
Text(message.author, style = MaterialTheme.typography.titleMedium)
Text(message.text, style = MaterialTheme.typography.bodyMedium)
}
Text(
text = Instant.ofEpochMilli(message.timestamp)
.atZone(ZoneId.systemDefault())
.toLocalTime().toString(),
style = MaterialTheme.typography.labelSmall
)
}
}
Key takeaways:
- Slot API surfaces extension points (topBar, FAB) so parents can compose structure without bloating parameters. (developer.android.com)
- keys in LazyColumn preserve identity across insert/reorder, preventing needless disposal and effect restarts. (developer.android.com)
- rememberSaveable for UI state that must survive Activity recreation/process death; use rememberSerializable for complex serializable types. (developer.android.com)
- With strong skipping, more recompositions are safely skipped; lambdas are memoized under the hood. Still, don’t mutate collections in place—see pitfalls. (developer.android.com)
Common Pitfalls and Tradeoffs
- Forgetting idempotency
- Symptom: logging/toasts/network calls inside composables cause duplicate work as recomposition re-executes code.
- Fix: move side effects to LaunchedEffect, DisposableEffect, SideEffect, etc. Keep UI code pure. (developer.android.com)
- Misunderstanding positional memoization
- Symptom: remembered values “reset” after reordering UI.
- Fix: preserve identity with key(…) or use Lazy* APIs that accept item keys; consider movableContentOf for advanced cases. (developer.android.com)
- In-place mutation of collections (especially with strong skipping)
- Symptom: UI doesn’t update or skips unexpectedly because unstable parameters are compared by instance identity; mutating the same List reference won’t look “changed” to the runtime.
- Fix: use new List instances (copy), persistent immutable collections, or snapshot-backed state lists; avoid in-place mutation. (developer.android.com)
- Over-hoisting or under-hoisting state
- Symptom: either too many parameters and recomposition ripples, or tight coupling and hard-to-reuse components.
- Fix: hoist state that callers must control; keep ephemeral UI state (scroll, animation) local via remember/rememberSaveable. (developer.android.com)
- Ignoring identity in dynamic UIs
- Symptom: reorder/insert causes items to recreate, losing focus/scroll/effects.
- Fix: always provide stable keys for items. (developer.android.com)
- Assuming “skipping makes everything fast”
- Tradeoff: skipping reduces work but correctness wins. Favor clarity and purity first, then measure and tune with tools/codelabs on stability/skipping. (developer.android.com)
Technical Note
You may see mixed messaging across articles and docs on “strong skipping mode” (introduced in the Compose compiler 1.5.4 era). Historically it was opt-in; guidance later announced it becoming production-ready and enabled by default in newer compiler versions. What you should do today (as of 2026-03-16):
- Verify your Compose compiler plugin version and read its release notes. Some docs still describe enabling strong skipping per module; others assume it’s on. Behavior (like lambda memoization) depends on the compiler in use. (developer.android.com)
Sources & Further Reading
- Thinking in Compose (Declarative UI, recomposition): https://developer.android.com/develop/ui/compose/mental-model (developer.android.com)
- Phases (composition, layout, drawing): https://developer.android.com/develop/ui/compose/phases (developer.android.com)
- Lifecycle of composables (enter/stay/leave, keys): https://developer.android.com/develop/ui/compose/lifecycle (developer.android.com)
- Side-effects in Compose (keep composables pure): https://developer.android.com/develop/ui/compose/side-effects (developer.android.com)
- State lifespans & positional memoization (remember, rememberSaveable, rememberSerializable): https://developer.android.com/develop/ui/compose/state-lifespans (developer.android.com)
- Slot APIs and layout basics: https://developer.android.com/develop/ui/compose/layouts/basics (developer.android.com)
- Best practices for performance (keys, stability): https://developer.android.com/develop/ui/compose/performance/bestpractices (developer.android.com)
- Strong skipping mode overview: https://developer.android.com/develop/ui/compose/performance/stability/strongskipping (developer.android.com)
- Compose Compiler and Runtime release notes: https://developer.android.com/jetpack/androidx/releases/compose-compiler and https://developer.android.com/jetpack/androidx/releases/compose-runtime (developer.android.com)
- Community note on list mutation with strong skipping: https://newsletter.jorgecastillo.dev/p/strong-skipping-does-not-fix-kotlin-collections (newsletter.jorgecastillo.dev)
Check Your Work
Hands-on Exercise
- Take a dynamic list screen and:
- Replace any configuration booleans with slot APIs where appropriate (e.g., allow callers to provide a topBar).
- Ensure items provide stable keys; then reorder data and verify items retain focus/animation state.
- Convert any in-place List mutations to produce new List instances. With a profiler/inspector, confirm recomposition only for changed items.
- Move any logging/network calls out of composables into LaunchedEffect/DisposableEffect. Confirm that quick state toggles no longer duplicate side effects.
Brain Teaser
- Suppose you have a composable that accepts:
- a data class User(name: String), and
- a lambda onClick: () -> Unit capturing a mutable List inside a ViewModel. After enabling a newer compiler with strong skipping, you notice fewer recompositions—but sometimes the UI fails to reflect underlying List changes. Explain precisely:
- Why strong skipping caused fewer recompositions here.
- Why the UI might not update when the List mutates in place.
- Two fixes that maintain correctness and keep skipping effective (hint: immutable/persistent lists or snapshot-backed state, and stable identity via keys). Tie your explanation to the composition tree, positional memoization, and idempotency principles. (developer.android.com)
If you can both implement the exercise and articulate the brain teaser clearly, you’ve refreshed the nine fundamentals: @Composable, Declarative UI, Composition Tree, Recomposition, Idempotency, Composable Lifecycle, Positional Memoization, Pure Composables, and Slot API.
References
- developer.android.com/develop/ui/compose/mental-model
- developer.android.com/develop/ui/compose/phases
- developer.android.com/develop/ui/compose/lifecycle
- developer.android.com/develop/ui/compose/side-effects
- developer.android.com/develop/ui/compose/state-lifespans
- developer.android.com/develop/ui/compose/layouts/basics
- developer.android.com/develop/ui/compose/performance/bestpractices
- developer.android.com/develop/ui/compose/performance/stability/strongskipping
- developer.android.com/jetpack/androidx/releases/compose-compiler
- newsletter.jorgecastillo.dev/p/strong-skipping-does-not-fix-kotlin-collections
- developer.android.com/develop/ui/compose/mental-model
- developer.android.com/develop/ui/compose/phases
- developer.android.com/develop/ui/compose/lifecycle
- developer.android.com/develop/ui/compose/side-effects
- developer.android.com/develop/ui/compose/state-lifespans
- developer.android.com/develop/ui/compose/layouts/basics
- developer.android.com/develop/ui/compose/performance/bestpractices
- developer.android.com/develop/ui/compose/performance/stability/strongskipping
- developer.android.com/jetpack/androidx/releases/compose-compiler
- developer.android.com/jetpack/androidx/releases/compose-runtime
- newsletter.jorgecastillo.dev/p/strong-skipping-does-not-fix-kotlin-collections
Share
More to explore
Keep exploring
3/7/2026
Jetpack Compose: Core Concepts Refresher
A spaced-repetition refresher on Core Concepts in Jetpack Compose, focused on practical implementation details and updates.
3/6/2026
Jetpack Compose: Core Concepts Deep Dive (Part 2/2)
A detailed learning-in-public deep dive on Core Concepts in Jetpack Compose, covering concept batch 2/2.
3/4/2026
Jetpack Compose: Core Concepts Deep Dive (Part 1/2)
A detailed learning-in-public deep dive on Core Concepts in Jetpack Compose, covering concept batch 1/2.
Previous
Flutter: Core Widget Concepts Refresher
Next