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
- Conceptual Model & Analogy
- Deep Dive
- Implementation Patterns
- Baseline Example (pure, declarative, and skippable)
- Production-Grade Example (positional memoization, effects, visibility, and new annotations)
- Common Pitfalls and Tradeoffs
- Technical Note
- Sources & Further Reading
- Check Your Work
Context and Scope
This is a fundamentals-level refresher on Jetpack Compose core concepts, reviewed on 2026-03-06 and updated for 2026-03-07. We’ll walk through exactly nine must-know ideas used daily in production Compose apps: @Composable, Declarative UI, Composition Tree, Recomposition, Idempotency, Composable Lifecycle, Positional Memoization, Pure Composables, and the Slot API. Along the way, you’ll see current guidance, recent 2025–2026 changes, practical usage patterns, pitfalls, and migration notes backed by official sources. (developer.android.com)
Conceptual Model & Analogy
-
Think “render from state”: You describe what the UI should look like for a given state. Compose takes care of computing and applying the minimal changes—no manual findViewById or setText calls. Like declaring “this is tonight’s menu” and letting the kitchen orchestrate prep efficiently. (developer.android.com)
-
Composition Tree as a living blueprint: Each composable call creates or updates an invisible node in a tree that Compose maintains. It’s like a production line’s station map—Compose knows where everything sits and what changed. (developer.android.com)
-
Positional memoization like a seating chart: remember stores values based on “where” they appear in code. If you reshuffle seating without telling the host (no keys), guests’ orders get mixed up. Use keys/movable content to preserve identity when the order changes. (developer.android.com)
Deep Dive
- @Composable (must know)
- What it is: A Kotlin annotation that marks a function as “UI-producing.” The Compose compiler transforms these functions to drive the composition and rendering pipeline. Without it, Compose won’t track or render the function. (android.googlesource.com)
- Why it matters: Only composables participate in recomposition and the composition tree. Non-annotated functions are regular Kotlin and don’t affect UI directly. (developer.android.com)
- Declarative UI (must know)
- The model: Provide a function of state -> UI; Compose figures out minimal updates. You re-invoke composables with new inputs, not mutate views. Benefits: reduced bugs and simpler mental model. (developer.android.com)
- Composition Tree
- The structure: A tree of nodes representing every composable call site (identity = where the call occurs). Compose stores bookkeeping and “slots” (for remembered values) to diff and apply updates efficiently. (developer.android.com)
- Identity: Call site + execution order (unless you supply keys) defines node identity; keys help preserve identity across reordering. (developer.android.com)
- Recomposition (must know)
- Definition: When relevant state changes, Compose reruns the affected composables (and only those) to refresh the screen. Skipping occurs when inputs are stable and unchanged. (developer.android.com)
- Optimistic/cancelable: Recomposition can be started, canceled, and restarted if inputs change mid-flight—so don’t rely on side effects during composition. (developer.android.com)
- Idempotency
- Contract: Running the same composable with the same inputs produces the same UI and no external side effects. Idempotency enables skipping, reordering, and cancellation without breaking app logic. (developer.android.com)
- Composable Lifecycle
- Stages: Entering the composition, zero-or-more recompositions, and leaving the composition. This lifecycle is simpler than Views; interact with external lifecycles via effects (e.g., LaunchedEffect, DisposableEffect) instead of inside composition. (developer.android.com)
- Multiple instances: Each call instance has its own lifecycle; use keys to stabilize identity when looping. (developer.android.com)
- Positional Memoization
- Mechanism: remember stores values in a slot table keyed by position in the composition tree. Move code around or change order without keys, and remembered values may “belong” to different instances. (android.googlesource.com)
- Lifespans: remember is shortest-lived (for recomposition). rememberSaveable persists across recreation via Bundle. Both are position-based; keys or movable content preserve identity. (developer.android.com)
- Pure Composables
- Definition: Composables with no hidden side effects; they render purely from inputs. Offload side effects to dedicated effect APIs for determinism and performance. (developer.android.com)
- Slot API
- Pattern: Accept UI as @Composable lambda parameters (e.g., content) so callers inject arbitrary UI while the parent handles structure and logic. This is how Button, Scaffold, etc., stay flexible. Use it in your own components for reusability. (android.googlesource.com)
Implementation Patterns
-
State hoisting + pure rendering
- Keep composables pure; lift state to owners (e.g., ViewModel) or pass state/change callbacks in. Effects run in LaunchedEffect/DisposableEffect, not inline. (developer.android.com)
-
Stable parameters and skipping
- Prefer immutable data classes or mark types @Stable when they satisfy stability rules. This increases skip opportunities and reduces recomposition work. (developer.android.com)
-
Correctly using remember/rememberSaveable
- Use remember for ephemeral UI state (animation, scroll), rememberSaveable for user input you must restore after process death/config changes. Supply keys to invalidate caches intentionally. (developer.android.com)
-
Keys for identity
- When emitting variable-count children, provide keys (items(key = …)) to preserve identity and avoid restarting effects unnecessarily. (developer.android.com)
-
Avoid reading frequently changing values in composition
- Mark getters as @FrequentlyChangingValue in libraries and move reads to derivedStateOf, snapshotFlow + LaunchedEffect, or to measure/layout/draw when appropriate. (developer.android.com)
-
Library factories that must be remembered
- Annotate constructors/factories that return stateful/expensive objects with @RememberInComposition so lint requires callers to remember them. (developer.android.com)
-
Slot APIs by default
- Prefer composable content parameters (“slots”) in reusable components to reduce parameter explosion and keep structure flexible. Follow Compose component API guidelines. (android.googlesource.com)
-
Visibility-driven work
- For impression logging or autoplay, prefer onFirstVisible/onVisibilityChanged over using effect start times—they’re designed for visibility, and prefetching can run composition early. (android-developers.googleblog.com)
Baseline Example (pure, declarative, and skippable)
@Composable
fun GreetingCard(
name: String,
modifier: Modifier = Modifier,
// Slot API: allow callers to provide trailing content (e.g., actions)
trailingContent: @Composable RowScope.() -> Unit = {}
) {
// Pure composable: reads inputs only, no side effects.
Row(
modifier
.padding(16.dp)
.background(MaterialTheme.colorScheme.surface, RoundedCornerShape(12.dp))
.padding(12.dp),
verticalAlignment = Alignment.CenterVertically
) {
Text(
text = "Hello, $name",
style = MaterialTheme.typography.titleMedium,
modifier = Modifier.weight(1f)
)
Spacer(Modifier.width(8.dp))
Row(content = trailingContent)
}
}
@Composable
fun GreetingScreen(
names: List<String>,
onClick: (String) -> Unit
) {
// Keys preserve identity even if 'names' is re-ordered.
LazyColumn {
items(names, key = { it }) { name ->
GreetingCard(name) {
Button(onClick = { onClick(name) }) { Text("Wave") }
}
}
}
}
- Declarative: UI is a function of inputs.
- Slot API: trailingContent parameter.
- Recomposition-friendly: Pure, skippable when inputs unchanged; identity preserved via keys. (developer.android.com)
Production-Grade Example (positional memoization, effects, visibility, and new annotations)
// Library or shared module: require remembering expensive/stateful objects.
@androidx.compose.runtime.annotation.RememberInComposition
fun createFormatter(locale: Locale): NumberFormat = NumberFormat.getCurrencyInstance(locale)
// Mark a frequently changing getter to discourage reading it directly in composition.
class ScrollSignals(private val listState: LazyListState) {
@androidx.compose.runtime.annotation.FrequentlyChangingValue
fun offset(): Int = listState.firstVisibleItemScrollOffset
}
@Composable
fun ProductFeed(
items: List<Product>,
modifier: Modifier = Modifier,
onImpression: (id: String) -> Unit,
onOpen: (id: String) -> Unit
) {
// Positional memoization: save across recompositions and process death when possible.
// Cache window and visibility APIs require recent Compose UI versions.
val listState = rememberLazyListState() // consider rememberSaveable(listState) when appropriate
// Avoid reading frequently changing scroll values in composition.
// Instead, convert to a Flow and handle in an effect.
val signals = remember { ScrollSignals(listState) }
val topAppBarElevated by remember {
derivedStateOf { listState.firstVisibleItemIndex > 0 || signals.offset() > 0 }
}
// Use snapshotFlow to react to rapid changes without forcing recomposition of whole subtree.
LaunchedEffect(listState) {
snapshotFlow { listState.firstVisibleItemIndex > 0 || signals.offset() > 0 }
.distinctUntilChanged()
.collect { /* update analytics, throttle as needed */ }
}
// Locale-dependent, expensive formatter: enforced remembering by annotation + lint.
val currencyFormat = remember(Locale.getDefault()) { createFormatter(Locale.getDefault()) }
Column(modifier.fillMaxSize()) {
TopAppBar(
title = { Text("Products") },
// Minimal recompositions: derivedStateOf feeds this single read.
colors = TopAppBarDefaults.topAppBarColors(
containerColor = if (topAppBarElevated) MaterialTheme.colorScheme.surfaceVariant
else MaterialTheme.colorScheme.surface
)
)
// Prefetching means effects inside items can run early; don't treat them as "visible".
LazyColumn(state = listState, modifier = Modifier.fillMaxSize()) {
items(items, key = { it.id }) { product ->
// Visibility-driven work: use onFirstVisible for impressions.
Row(
Modifier
.fillMaxWidth()
.padding(16.dp)
.onFirstVisible(minDurationMs = 500) { onImpression(product.id) }
.clickable { onOpen(product.id) }
) {
Text(
text = product.title,
style = MaterialTheme.typography.titleMedium,
modifier = Modifier.weight(1f)
)
Text(
text = currencyFormat.format(product.priceCents / 100.0),
style = MaterialTheme.typography.bodyMedium
)
}
Divider()
}
}
}
}
- Positional memoization: remember and derivedStateOf maintain cheap, localized updates.
- Effects: snapshotFlow + LaunchedEffect handles frequently changing values without recomposing large subtrees.
- Visibility APIs: onFirstVisible avoids using composition start as a “visibility” signal, which is brittle under prefetching.
- Lint-backed annotations: @RememberInComposition/@FrequentlyChangingValue guide usage and tooling. (developer.android.com)
Common Pitfalls and Tradeoffs
-
Doing work during composition
- Don’t start coroutines, I/O, or log/track from composable bodies; composition can re-run, skip, cancel, or run out of order. Use effect APIs instead. (developer.android.com)
-
Misplaced remember leading to “state jumps”
- Moving or reordering remembered calls without keys can attach the wrong state to instances. Add key(…) or stable item keys, or restructure with movableContentOf when needed. (developer.android.com)
-
Reading frequently changing values directly in composition
- Scroll offsets/animated values cause rapid recomposition. Prefer derivedStateOf, snapshotFlow in LaunchedEffect, or move reads to layout/draw phases when appropriate. Consider @FrequentlyChangingValue for library APIs. (developer.android.com)
-
Forgetting stability
- Mutable parameters or unstable lambdas reduce skipping. Favor immutable types, or mark compliant types @Stable. Consider strong skipping mode only after measuring trade‑offs. (developer.android.com)
-
Treating composition timing as “visibility”
- Prefetch can compose offscreen items; run impression/autoplay via onVisibilityChanged/onFirstVisible. (android-developers.googleblog.com)
Technical Note
-
Runtime annotations and lint (2025)
- @Stable, @Immutable, and @StableMarker moved into a separate runtime-annotation artifact so you can annotate types in non-Compose modules. New @RememberInComposition and @FrequentlyChangingValue ship with lint checks; AGP/Lint 8.8.2+ required for these checks. Migrate imports and update tooling. (developer.android.com)
-
Prefetch and visibility
- Lazy layout cache windows and prefetching can trigger composition/effects early. Don’t infer visibility from effects; use onFirstVisible/onVisibilityChanged. (android-developers.googleblog.com)
-
VectorPainter deprecation note
- Some painter APIs were deprecated in favor of remember-prefixed variants to better communicate internal caching semantics; update usages accordingly. (developer.android.com)
Sources & Further Reading
- Thinking in Compose (Declarative model, recomposition, idempotency): https://developer.android.com/develop/ui/compose/mental-model (developer.android.com)
- Lifecycle of composables (enter/recompose/leave, keys, stability): https://developer.android.com/develop/ui/compose/lifecycle (developer.android.com)
- State and Jetpack Compose (remember, rememberSaveable, keys): https://developer.android.com/develop/ui/compose/state (developer.android.com)
- State lifespans and positional memoization: https://developer.android.com/develop/ui/compose/state-lifespans (developer.android.com)
- Side-effects in Compose (LaunchedEffect, DisposableEffect, snapshotFlow): https://developer.android.com/develop/ui/compose/side-effects (developer.android.com)
- Compose Runtime release notes (runtime-annotation, new lint checks): https://developer.android.com/jetpack/androidx/releases/compose-runtime (developer.android.com)
- API refs: @Composable annotation: https://developer.android.com/reference/kotlin/androidx/compose/runtime/Composable (android.googlesource.com)
- API refs: @RememberInComposition and @FrequentlyChangingValue:
- How Compose Works (Composer, Slot Table, positional memoization): https://android.googlesource.com/platform/frameworks/support/+/HEAD/compose/runtime/design/how-compose-works.md (android.googlesource.com)
- Compose Component API Guidelines (Slot APIs): https://android.googlesource.com/platform/frameworks/support/+/androidx-main/compose/docs/compose-component-api-guidelines.md (android.googlesource.com)
- Compose UI release notes (VectorPainter deprecation; composition changes; Popup fix): https://developer.android.com/jetpack/androidx/releases/compose-ui (developer.android.com)
- What’s new in Jetpack Compose (Aug 2025: visibility modifiers, annotations, cache windows): https://android-developers.googleblog.com/2025/08/whats-new-in-jetpack-compose-august-25-release.html (android-developers.googleblog.com)
Check Your Work
Hands-on Exercise
- Build a “Messages” screen:
- Define a pure MessageRow composable that renders from inputs only.
- Use LazyColumn with items(key = { message.id }).
- Add a trailingContent slot to MessageRow for per-item actions.
- Implement an impression logger with onFirstVisible(minDurationMs = 500).
- Add a toolbar that elevates when the list is not at the top, using derivedStateOf and snapshotFlow to avoid recomposing the whole list on scroll.
- Refactor any expensive allocators into factories annotated with @RememberInComposition; remember them in composition.
- Verify with Layout Inspector that only affected rows recompose when toggling a message’s “starred” state. (developer.android.com)
Brain Teaser
- You have a Column that loops over a mutable list of items without keys, each row remembering its own local animation state. A new item is inserted at index 0. Explain precisely what happens to each row’s remembered animation state and why—and outline two ways to preserve per-item state after the insertion. (Hint: think composition identity, positional memoization, and keys.) (developer.android.com)
References
- 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/state-lifespans
- developer.android.com/develop/ui/compose/side-effects
- developer.android.com/jetpack/androidx/releases/compose-runtime
- android.googlesource.com/platform/frameworks/support/%2B/54d616ad45cd299432d4872fad96f406da51465d/compose/runtime/design/how-compose-works.md
- developer.android.com/reference/kotlin/androidx/compose/runtime/annotation/RememberInComposition
- android.googlesource.com/platform/frameworks/support/%2B/HEAD/compose/runtime/design/how-compose-works.md
- android.googlesource.com/platform/frameworks/support/%2B/androidx-main/compose/docs/compose-component-api-guidelines.md
- developer.android.com/jetpack/androidx/releases/compose-ui
- android-developers.googleblog.com/2025/08/whats-new-in-jetpack-compose-august-25-release.html
- developer.android.com/develop/ui/compose/state
- developer.android.com/develop/ui/compose/state-lifespans
- developer.android.com/develop/ui/compose/side-effects
- developer.android.com/jetpack/androidx/releases/compose-runtime
- developer.android.com/reference/kotlin/androidx/compose/runtime/Composable
- developer.android.com/reference/kotlin/androidx/compose/runtime/annotation/RememberInComposition
- developer.android.com/reference/kotlin/androidx/compose/runtime/annotation/FrequentlyChangingValue
- android.googlesource.com/platform/frameworks/support/+/HEAD/compose/runtime/design/how-compose-works.md
- android.googlesource.com/platform/frameworks/support/+/androidx-main/compose/docs/compose-component-api-guidelines.md
- developer.android.com/jetpack/androidx/releases/compose-ui
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/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
Weekly Engineering Mastery Quiz (2026-03-02 to 2026-03-06)
Next