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 is a focused refresher on nine fundamental Jetpack Compose core concepts. The goal is to translate mental models into day-to-day engineering practice, highlight recent runtime/compiler changes relevant in 2025–2026, and provide concrete patterns and pitfalls. Last reviewed: 2026-03-16; run date: 2026-04-07.
Covered in detail here (exactly nine):
- @Composable 2) Declarative UI 3) Composition Tree 4) Recomposition 5) Idempotency 6) Composable Lifecycle 7) Positional Memoization 8) Pure Composables 9) Slot API
Conceptual Model & Analogy
Think of Compose as a stage play directed from a script:
- The script is your declarative UI: you describe scenes for a given story state.
- Actors on stage form the composition tree: a hierarchy of performers and props.
- When the plot changes, the director doesn’t rebuild the entire set; only cues the affected actors to adjust lines or positions. That cueing is recomposition.
- The director expects actors to say the same lines given the same cues every time (idempotency, pure composables). If an actor ad‑libs (side-effects), the show becomes unpredictable.
- Props remembered “by where they appear on stage” (positional memoization) are set down in exact spots so stagehands can pick the right one up after a scene change.
This framing helps you keep composables fast, predictable, and easy to reason about. (developer.android.com)
Deep Dive
- @Composable: “this function draws UI”
- Mark a function with @Composable to opt into the Compose compiler/runtime. Without it, Compose cannot call, track, or render that function in composition. Composable functions are invoked to emit UI; they typically return Unit. (developer.android.com)
- You can call composables only from other composables (or supported host environments like setContent). The compiler transforms these calls and wires them into the runtime. (developer.android.com)
- Declarative UI
- You describe what the UI should look like for the current state. When state changes, you call the same composables with new inputs instead of mutating widgets. Compose figures out the minimal set of updates. (developer.android.com)
- Composition Tree
- On initial composition, the runtime executes your composables and builds a data structure describing UI nodes. This “UI tree” is the basis for later layout and drawing phases. It’s not a View tree; it’s Compose’s own representation. (developer.android.com)
- Internally, Compose stores bookkeeping and remembered values in a SlotTable; recent runtime updates rewrote this structure to accelerate recomposition, especially when reordering long lists. (developer.android.com)
- Recomposition
- When observed state read during composition changes, Compose selectively re-invokes only the affected functions/lambdas, skipping the rest. Recomposition is optimistic and may be canceled/restarted. (developer.android.com)
- Compiler/runtime “strong skipping mode” now memoizes more lambdas and makes more composables skippable by default (Kotlin 2.0.20+), reducing unnecessary work. (developer.android.com)
- Idempotency
- A composable must produce the same result given the same inputs, regardless of how often it’s re-run or skipped. Compose may re-execute frequently (e.g., every frame during animation) or in parallel/out-of-order; idempotency keeps behavior correct. (developer.android.com)
- Composable Lifecycle
- Each composable instance goes through: enter composition → 0+ recompositions → leave composition. Write code assuming it can be skipped, canceled, or abandoned mid-frame. For setup/teardown aligned with this lifecycle, use the Side-Effect APIs or observer interfaces. (developer.android.com)
- Positional Memoization
- remember stores a value in the Composition “by position” at the call site. It returns the cached value on recomposition and forgets it when the calling composable leaves the Composition (or when keys change). This is the basis of positional memoization. (developer.android.com)
- In lists, provide stable item keys so remembered state moves with items when their positions change. (developer.android.com)
- Pure Composables
- Keep composables side-effect free; they should only derive UI from parameters/state. Trigger effects (I/O, logging, navigation, analytics) via dedicated Effect APIs like LaunchedEffect, SideEffect, DisposableEffect, or from event callbacks. (developer.android.com)
- Slot API
- A pattern to pass composable content as parameters (e.g., content: @Composable () -> Unit). Material/Scaffold, AppBars, BottomSheets expose multiple named slots (title, navigationIcon, actions, floatingActionButton) that you can fill to customize layout without bloating parameter lists. (developer.android.com)
Implementation Patterns
Baseline example (must-know principles: @Composable, declarative, idempotent, pure, slot use)
@Composable
fun CounterRow(
count: Int,
onIncrement: () -> Unit,
trailing: @Composable () -> Unit = {} // Slot API for extensibility
) {
// Pure & idempotent: UI depends only on inputs
Row(Modifier.padding(16.dp), verticalAlignment = Alignment.CenterVertically) {
Text("Count: $count", style = MaterialTheme.typography.bodyLarge)
Spacer(Modifier.width(8.dp))
Button(onClick = onIncrement) { Text("Add") }
Spacer(Modifier.weight(1f))
trailing() // caller-provided content
}
}
- This composable is annotated with @Composable, is declarative, and stays pure/idempotent. The trailing slot lets callers inject any UI, a common pattern used by Scaffold/AppBars. (developer.android.com)
Production-grade example (positional memoization, recomposition control, lifecycle, and slots)
@Composable
fun ProductListScreen(
products: List<Product>, // immutable list preferred for stability
onProductClick: (ProductId) -> Unit,
modifier: Modifier = Modifier
) {
// Query is user-editable state that should survive simple config changes & process death
var query by rememberSaveable { mutableStateOf("") } // positional + saveable
val filtered by remember(query, products) {
derivedStateOf { products.filter { it.matches(query) } }
}
// Example of "retained" state: a cache that should survive configuration but not process death
// Avoid retaining Context; retain only safe, non-Context objects.
val thumbnailCache = androidx.compose.runtime.retain.retain { ThumbnailCache(maxEntries = 200) }
Column(modifier) {
TopAppBar(
title = { Text("Catalog") },
actions = { /* Slot: search, sort */ }
)
OutlinedTextField(
value = query,
onValueChange = { query = it },
leadingIcon = { Icon(Icons.Default.Search, null) }
)
// Stable keys ensure per-item remembered state travels with its item when positions change.
LazyColumn(
modifier = Modifier.fillMaxSize(),
contentPadding = PaddingValues(12.dp),
verticalArrangement = Arrangement.spacedBy(12.dp)
) {
items(
items = filtered,
key = { it.id.value } // critical for remembered state locality
) { product ->
ProductCard(
product = product,
onClick = { onProductClick(product.id) },
// Slot to customize trailing actions per screen
trailing = {
val thumb = remember(product.id) {
thumbnailCache.loadOrNull(product.thumbnailUrl)
}
if (thumb != null) {
Image(bitmap = thumb, contentDescription = null, modifier = Modifier.size(40.dp))
}
}
)
}
}
}
}
@Composable
private fun ProductCard(
product: Product,
onClick: () -> Unit,
trailing: @Composable () -> Unit
) {
ElevatedCard(onClick = onClick) {
Row(Modifier.padding(16.dp), verticalAlignment = Alignment.CenterVertically) {
Column(Modifier.weight(1f)) {
Text(product.name, style = MaterialTheme.typography.titleMedium)
Text(product.subtitle, style = MaterialTheme.typography.bodyMedium)
}
trailing() // Slot API
}
}
}
- remember/rememberSaveable cache values by position in the composition; keys force invalidation when inputs change. Use stable keys in Lazy lists so state follows the item. Retain is now available for values that should outlive temporary composition removal (e.g., back-stack screens), but never retain Context-bound objects. Keep composables pure; any I/O should live in effects or higher layers. (developer.android.com)
Common Pitfalls and Tradeoffs
- Calling non-pure work in composition
- Reading SharedPreferences, starting coroutines, or logging in a composable breaks idempotency and may fire unpredictably due to skipping/cancelation. Move to Effect APIs or ViewModel. (developer.android.com)
- Forgetting stable item keys in Lazy lists
- Without keys, remembered state is anchored to index, not identity; reorders will misassociate or lose state. Provide keys. (developer.android.com)
- Misusing remember vs rememberSaveable vs retain
- remember: survives recomposition only; forgets when leaving composition.
- rememberSaveable: also survives Activity recreation/process death for Bundle‑saveable data.
- retain: survives transient removal/re-creation of content (e.g., config change, hidden destinations) via a RetainedValuesStore; do not retain Contexts. Choose the narrowest lifetime that satisfies requirements. (developer.android.com)
- Assuming recomposition equals redraw/layout
- Compose tracks state reads across phases and may skip layout/draw if unchanged. Prefer layout-phase reads (e.g., lambda versions of modifiers) when only placement depends on quickly-changing state. (developer.android.com)
- Over-allocating lambdas/state each recomposition
- With strong skipping the compiler remembers more lambdas, but be mindful: captures of unstable objects can still affect skippability semantics. Avoid creating new objects in parameter lists unnecessarily. (developer.android.com)
- Relying on outdated concurrency guidance
- Experimental concurrent recomposition APIs have been removed; don’t depend on them for parallel UI work. Keep composables thread-safe and pure anyway. (developer.android.com)
Technical Note
- Runtime internals and performance flags
- Compose Runtime 1.11 alpha introduced a rewritten SlotTable behind a feature flag (ComposeRuntimeFlags.isLinkBufferComposerEnabled). It can speed up recomposition (notably list reorders) and may require proguard config changes in release builds to enable. Evaluate with profiling before turning it on app‑wide. (developer.android.com)
- Strong Skipping defaults and annotations
- Strong skipping is enabled by default in recent toolchains and automatically remembers many lambdas. If you intentionally need a fresh lambda on each recomposition, use @DontMemoize. (developer.android.com)
- New Retain API
- The retain API persists values across transient composition removal. It delivers RetainObserver callbacks (not RememberObserver), and forbids retaining Context-typed values; misuse risks leaks. Prefer ViewModel when scope spans screens/processes. (developer.android.com)
- Removed: experimental concurrent recomposition API
- If you encounter old posts advocating it, note it was removed in 1.11.0‑alpha01 (2025‑12‑03). (developer.android.com)
Sources & Further Reading
- Thinking in Compose (mental model, idempotency, recomposition) — https://developer.android.com/develop/ui/compose/mental-model (developer.android.com)
- Compose phases (composition tree, phases, optimized reads) — https://developer.android.com/develop/ui/compose/phases (developer.android.com)
- Lifecycle of composables — https://developer.android.com/develop/ui/compose/lifecycle (developer.android.com)
- State and Jetpack Compose (remember, rememberSaveable, derivedStateOf) — https://developer.android.com/develop/ui/compose/state (developer.android.com)
- Lists and grids (stable keys/state with items) — https://developer.android.com/develop/ui/compose/lists (developer.android.com)
- Side-effects in Compose (pure vs effects) — https://developer.android.com/develop/ui/compose/side-effects (developer.android.com)
- Slot APIs and layout basics — https://developer.android.com/develop/ui/compose/layouts/basics (developer.android.com)
- Strong skipping mode (compiler/runtime behavior) — https://developer.android.com/develop/ui/compose/performance/stability/strongskipping (developer.android.com)
- Compose Runtime release notes (retain, SlotTable rewrite, removals) — https://developer.android.com/jetpack/androidx/releases/compose-runtime (developer.android.com)
- Retain API reference — https://developer.android.com/reference/kotlin/androidx/compose/runtime/retain/retain.composable (developer.android.com)
Check Your Work
Hands-on Exercise
- Goal: Build a detail screen that remains pure, fast, and resilient to configuration change.
- Implement a pure ProductDetails(product: Product, onAddToCart: () -> Unit, footer: @Composable () -> Unit).
- In a host composable, hold a query string with rememberSaveable and filter a list via derivedStateOf.
- Use LazyColumn with stable keys; verify that per-item remembered UI (e.g., expansion toggle) persists across reorders.
- Add a cache object via retain for thumbnails or computed text layout. Prove you’re not retaining a Context by making it a pure Kotlin object.
- Profile recompositions (Layout Inspector recomposition counts). Confirm that filtering only recomposes affected rows and that lambda allocations are stable under strong skipping.
Brain Teaser
- Suppose you have:
- A composable Row that reads scrollOffset from LazyListState and applies Modifier.offset(y = scrollOffset/2).
- An expensive regex compiled in the composable body and passed to a child each frame.
- A LazyColumn without item keys that displays mutable data. Questions:
- Which change reduces recompositions without altering visuals: passing offset as a lambda to Modifier.offset or as a value? Why? (Hint: phase reads.) (developer.android.com)
- How do you avoid recompiling the regex yet keep correctness when its pattern input changes occasionally? (Hint: remember with keys/derivedStateOf.) (developer.android.com)
- When the list reorders, why might per-item expansion state jump to the wrong row, and how do you fix it? (Hint: stable item keys.) (developer.android.com)
- When would you choose retain over rememberSaveable or ViewModel for a cache-like object, and what must you never retain? (developer.android.com)
If you can justify each answer with a citation and point to where in your code the concept manifests, you’ve refreshed the nine core Compose fundamentals.
References
- developer.android.com/jetpack/compose/mental-model
- developer.android.com/develop/ui/compose/phases
- developer.android.com/develop/ui/compose/lifecycle
- developer.android.com/develop/ui/compose/state
- developer.android.com/develop/ui/compose/lists
- developer.android.com/develop/ui/compose/side-effects
- developer.android.com/develop/ui/compose/layouts/basics
- developer.android.com/develop/ui/compose/performance/stability/strongskipping
- developer.android.com/jetpack/androidx/releases/compose-runtime
- developer.android.com/reference/kotlin/androidx/compose/runtime/retain/retain.composable
- developer.android.com/develop/ui/compose/mental-model
- developer.android.com/develop/ui/compose/lifecycle
- developer.android.com/develop/ui/compose/state
- developer.android.com/develop/ui/compose/lists
- developer.android.com/develop/ui/compose/side-effects
- developer.android.com/develop/ui/compose/layouts/basics
- developer.android.com/develop/ui/compose/performance/stability/strongskipping
Share
More to explore
Keep exploring
3/16/2026
Jetpack Compose: Core Concepts Refresher
A spaced-repetition refresher on Core Concepts in Jetpack Compose, focused on practical implementation details and updates.
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.
Next