Free Jetpack Compose Interview Questions & Answers

Recomposition, state, side effects, and Compose performance.

All 50 questions and detailed answers are free. No account or sign-in required.

  1. What actually triggers a recomposition in Jetpack Compose?

    Recomposition is triggered when a State object that was read during a composable's execution changes value. Compose's snapshot system records exactly which State reads happened inside which recomposition scopes, so when you write a new value to a MutableState, only the scopes that read that specific state are invalidated and re-run. It is not the passage of a variable changing in general; a plain var that is reassigned will never trigger recomposition because Compose has no observation hook on it. Only reads of observable snapshot state (MutableState, or state-backed types like mutableStateListOf) participate.

  2. Why does hoisting state make a composable more reusable, and what is the standard hoisting shape?

    State hoisting moves the state out of a composable and passes the current value down plus an event lambda up, following unidirectional data flow. The canonical shape is a value parameter and an onValueChange callback, which makes the composable stateless, easier to test, and controllable by a single source of truth. The composable becomes a pure function of its inputs so the same widget can be driven by a ViewModel, a parent, or a test harness. The rule of thumb is to hoist to the lowest common ancestor that needs to read or write the state.

  3. What is the difference between remember and rememberSaveable, and when does each survive?

    remember caches a value across recompositions but loses it on configuration change or process death because it lives only in the composition. rememberSaveable additionally persists the value through configuration changes and system-initiated process death by storing it in the saved instance state Bundle. The catch is that rememberSaveable can only automatically store types that fit into a Bundle, so for custom types you must supply a Saver. Use remember for transient derived or expensive objects, and rememberSaveable for user-facing state like scroll position or text input that should survive rotation.

  4. What are the three phases of a Compose frame, and why does the distinction matter?

    Each frame runs composition (deciding what to show and building the tree of nodes), layout (measuring and placing each node), and drawing (rendering into the canvas). The distinction matters because a state read only invalidates the phase where it happens, so if you read state only during layout or draw you can skip recomposition entirely. This is the basis for the performance guidance to defer state reads to the latest possible phase. Reading a frequently changing value in composition forces the whole subtree to recompose, whereas reading it in a graphicsLayer or offset lambda only re-runs layout or draw.

  5. What makes a composable function skippable versus restartable?

    A restartable function is one the runtime can re-invoke on its own when its inputs change, which is nearly every non-inline composable that returns Unit. A skippable function is one the runtime can choose not to re-execute when it is called with the same arguments as last time, because all of its parameters are of stable types and are equal. Skippability is what actually saves work: a restartable-but-not-skippable function always re-runs when its parent recomposes. The Compose compiler decides these at build time and you can inspect the results in the compiler metrics report.

  6. Why can an unstable parameter type prevent a composable from being skipped?

    Compose can only skip a composable if it can prove each parameter has not changed by comparing to the previous value, which it only trusts for stable types whose equals reliably reflects meaningful change and whose public properties do not mutate unobserved. If a parameter is unstable, the compiler assumes it could have changed in a way Compose cannot detect, so it conservatively marks the function unskippable and re-runs it every time the parent recomposes. Common culprits are classes from modules without the Compose compiler, mutable collections like List backed by ArrayList, or classes with var properties. Marking a genuinely immutable type with the Immutable annotation, or using a stable collection, restores skipping.

  7. What is the difference between the Stable and Immutable annotations, and what contract do you promise?

    Immutable promises the object's public properties will never change after construction, so Compose can treat any two references as interchangeable if they are equal and skip aggressively. Stable is a weaker promise: the properties may change, but whenever they do the object will notify Compose through the snapshot system, and equals is consistent so that if equals returns true for two instances they will stay equal. Both are promises you make to the compiler that it cannot fully verify, so lying causes missed recompositions and stale UI. Use Immutable for value-like data classes with val properties, and Stable for observable holders.

  8. Why does passing a lambda to a composable sometimes break skipping, and how do you fix it?

    A lambda that captures a value from the enclosing scope is only stable if what it captures is stable and the lambda instance is remembered; an unstable capture, or a freshly allocated lambda each recomposition, can be treated as a changed argument. In practice the Compose compiler does memoize non-capturing and stably-capturing lambdas automatically so most callbacks are fine, but a lambda capturing an unstable receiver still poisons skipping. The fix is to ensure captured values are stable, hoist the lambda into a remember, or reference a method by stable reference. This is why an otherwise skippable child can still recompose every frame if its onClick captures an unstable object.

  9. When does derivedStateOf actually help, and when is it pointless?

    derivedStateOf helps when you compute a value from one or more frequently changing states but the computed result changes far less often, because it caches the result and only notifies readers when the derived output actually changes. The classic example is deriving whether a list is scrolled past the first item from a continuously changing scroll offset: the offset changes every pixel but the boolean flips rarely, so readers recompose only on the flip. It is pointless, and pure overhead, when the inputs and output change at the same frequency, or when you are just transforming a single state one to one. It is also wrong to use it for a value that depends on parameters rather than state; a plain calculation or remember with keys is correct there.

  10. In a LazyColumn, what does providing a stable key per item change, and what breaks without it?

    Providing a key derived from stable item identity lets Compose match items across data changes so that when the list is reordered, inserted into, or deleted from, it preserves the correct item state, animations, and scroll position rather than matching by position index. Without keys, Compose falls back to positional matching, so inserting an item at the top shifts every item's state down by one, causing remembered state, focus, and animations to attach to the wrong item. Keys also let Compose keep an item's internal remember values when it moves. The key must be unique and stable across recompositions, typically a database id, and must be Bundle-saveable if you rely on saved state.

  11. What is the purpose of the key composable, and how is it different from a LazyColumn item key?

    The key composable wraps a block and gives the state remembered inside it an identity tied to the key argument, so when the key changes Compose treats it as a different logical instance and resets its remember values, and when items reorder it moves their state with them. It is used anywhere identity matters within a normal composition, for example a loop of composables outside a lazy list where two items could otherwise share a slot. A LazyColumn item key serves the same identity purpose but is specialized to the lazy layout's item slots and also drives item animations and scroll retention. Both exist because Compose otherwise identifies state by call-site position, which is ambiguous under reordering.

  12. Why is capturing a changing value inside LaunchedEffect(Unit) a bug, and what are the two correct fixes?

    LaunchedEffect(Unit) starts its coroutine once and never restarts, so any value it captured at first composition is frozen; if that value later changes, the effect keeps using the stale original and never sees updates. If you want the effect to restart when the value changes, pass that value as a key so the coroutine is canceled and relaunched with the new value. If instead you want a long-lived effect that always reads the latest value without restarting, wrap the value in rememberUpdatedState and read that inside the effect. Choosing Unit as the key when the body depends on changing data is the classic mistake.

  13. What exactly do the keys of a LaunchedEffect control?

    The keys form the identity of the effect: when any key changes between recompositions, Compose cancels the currently running coroutine and launches a fresh one with the new captured values, and when the keys are unchanged the existing coroutine keeps running untouched. This means keys are how you express restart-on-change semantics, not merely a dependency list for correctness of a pure function. Passing too few keys leaves the effect running with stale inputs; passing an unstable key that changes every recomposition restarts the effect constantly and can cause an infinite relaunch loop. Choose keys as the precise set of inputs whose change should tear down and restart the work.

  14. How does rememberUpdatedState differ from just passing a value as a LaunchedEffect key?

    rememberUpdatedState keeps a single long-running effect alive while ensuring it always reads the freshest value, by storing the value in a State that is updated on each recomposition without restarting the effect. Passing the value as a key does the opposite: it tears down and relaunches the effect whenever the value changes. You use rememberUpdatedState for things like a timeout callback or a lambda that should reflect the latest closure but must not restart the timer. It is essentially the tool for capturing the current value inside an effect whose lifecycle you deliberately want to keep independent of that value.

  15. What is DisposableEffect for, and what is the consequence of forgetting onDispose?

    DisposableEffect runs setup when it enters the composition or its keys change and requires you to return an onDispose block that runs when it leaves the composition or before the keys-driven restart. It is meant for effects that acquire a resource needing explicit cleanup, such as registering and unregistering a listener, observer, or callback. Forgetting proper cleanup in onDispose leaks the registration: the listener stays attached after the composable is gone, or a duplicate is added on each key change, causing memory leaks and double-firing callbacks. The onDispose is mandatory precisely to make cleanup impossible to forget.

  16. When should you use SideEffect versus LaunchedEffect?

    SideEffect runs after every successful recomposition and is meant to publish Compose state to a non-Compose object that has no coroutine or lifecycle needs, like updating an analytics property or a third-party controller each time the composition settles. LaunchedEffect runs a coroutine tied to the composition's lifecycle with restart-on-key semantics, meant for suspending or long-running work. SideEffect gives no cancellation, no keys, and no coroutine; it simply guarantees the code runs only on committed compositions, not on discarded ones. Reaching for SideEffect to do async work is wrong because it cannot suspend and runs on every frame that recomposes.

  17. What does produceState do that a LaunchedEffect plus a MutableState does not?

    produceState packages the common pattern of launching a coroutine that pushes values into a State and exposing that State as a read-only result, giving you a State backed by an effect whose lifecycle is tied to the composition. It offers an initial value, a coroutine scope where you assign to value, and an awaitDispose hook for cleanup of non-suspending sources. Functionally you could build the same with remember of a MutableState plus a LaunchedEffect, but produceState is the idiomatic, less error-prone form for converting an external async source into observable state. It is especially handy for one-shot or subscription-style data feeding a single piece of UI state.

  18. What problem does snapshotFlow solve, and what is a common misuse?

    snapshotFlow converts Compose snapshot state reads into a cold Flow that emits when any state read in its block changes, letting you bridge Compose state into Flow operators like debounce, distinctUntilChanged, or filter. A typical use is observing a lazy list's first visible item index and reacting only when it crosses a threshold. A common misuse is expecting it to emit for non-snapshot values or plain variables, which it cannot observe, or forgetting that it emits on every snapshot change so you should apply distinctUntilChanged to avoid redundant downstream work. It must be collected inside a coroutine, typically within a LaunchedEffect.

  19. What is the difference between mutableStateOf holding a List and using mutableStateListOf?

    mutableStateOf holding a List observes only reassignment of the whole list reference, so to trigger recomposition you must replace the list with a new instance, and a plain List is treated as immutable by readers. mutableStateListOf is an observable list whose individual add, remove, and set operations are themselves snapshot-tracked, so mutating it in place triggers recomposition without creating a new list. mutableStateListOf is also a stable type, whereas a plain mutable list inside state can hurt skippability. Choose mutableStateOf of an immutable list for whole-list swaps and clarity, and mutableStateListOf when you need efficient fine-grained in-place mutation.

  20. Does Modifier order matter, and can you give a case where reordering changes behavior?

    Modifier order is significant because modifiers wrap each other outside-in for constraints and inside-out for drawing, so each modifier operates on the result of the ones before it. For example padding then background paints the background only inside the padded region, while background then padding paints the full area and then insets the content, producing visibly different results. Similarly clickable then padding makes the padding non-clickable, whereas padding then clickable extends the touch target over the padding. Size, clip, and click behavior all depend on chain position, so modifier order is part of the component's semantics, not a stylistic detail.

  21. Why prefer passing a lambda to Modifier.offset or graphicsLayer instead of the value-taking overload?

    The lambda overloads of offset and the graphicsLayer block defer reading the changing state until the layout or draw phase, so when that state changes only layout or drawing re-runs and composition is skipped entirely. The value-taking overload reads the state during composition, which invalidates the composable and forces recomposition on every change. For high-frequency updates like animations or scroll-driven translation, this difference is dramatic because it turns per-frame recompositions into cheap layout or draw passes. This is the concrete application of deferring state reads to a later phase.

  22. What is the difference between a static and a dynamic CompositionLocal, and what is the cost of each?

    compositionLocalOf is dynamic: Compose tracks reads and, when the provided value changes, recomposes only the composables that actually read that local. staticCompositionLocalOf does not track reads, so changing its value forces the entire content under the provider to recompose regardless of who reads it, but reading it is cheaper because there is no tracking bookkeeping. Choose staticCompositionLocalOf for values that essentially never change during the composition, like a theme constant or a resource reference, and compositionLocalOf for values that change and are read in a few places. Getting this backwards either wastes tracking overhead or over-recomposes large subtrees.

  23. When is CompositionLocal the right tool versus just drilling a parameter?

    CompositionLocal is appropriate for ambient, cross-cutting values that many composables at many depths might need but that most intermediate composables should not have to name, such as theme, typography, or the current Android context. Parameter drilling is preferable when the dependency is explicit and important to a component's contract, because passing it as a parameter makes data flow visible and testable. Overusing CompositionLocal hides dependencies and makes a composable's behavior depend on invisible ambient state, hurting reusability and testability. The guideline is that a CompositionLocal should have a sensible default and represent a genuinely tree-wide concern.

  24. What is the difference between Modifier.composed and the Modifier.Node API, and why is the newer one preferred?

    Modifier.composed is a factory that runs composable code, like remember, when the modifier is materialized, but it defeats modifier reuse and skipping because each usage creates state during composition and cannot be compared or shared efficiently. The Modifier.Node API instead defines a lightweight node with explicit lifecycle and update logic that Compose can allocate, reuse, and update without recomposition, avoiding per-use composition overhead. Modifier.Node is preferred for custom modifiers because it is significantly cheaper, does not allocate on every composition, and integrates directly with the layout and draw phases. composed is effectively deprecated guidance for new custom modifiers.

  25. In a custom Layout, what are the rules around measuring children and calling place?

    Inside a Layout's MeasurePolicy you must measure each child measurable exactly once by calling measure with constraints, receiving a Placeable, and then in the layout block position each placeable with placeRelative or place. Measuring a child more than once throws, because Compose enforces single-pass measurement for performance and to preserve intrinsics correctness. You are responsible for choosing your own size within the incoming constraints and for placing every child you measured, otherwise children will not appear. The separation of a measure step returning placeables and a place step positioning them is what lets Compose lay out in a single pass.

  26. Why is SubcomposeLayout more expensive than Layout, and when is it justified?

    SubcomposeLayout defers composing some children until the measure phase so their content can depend on measurements of other children, which means composition happens inside layout rather than before it, breaking the normal phase separation and preventing some optimizations. This extra subcomposition has real cost and can trigger additional work, so it is not free the way a plain Layout is. It is justified when a child's very existence or content genuinely depends on available space or another child's size, as in BoxWithConstraints, LazyColumn's item windowing, or a component that sizes one slot based on another. If you do not need measurement-dependent composition, a plain Layout or intrinsics is cheaper.

  27. How far does a recomposition propagate when a single state changes?

    Recomposition propagates only to the recomposition scopes that read the changed state, where a scope is roughly the body of a restartable composable function. Compose invalidates the nearest enclosing restartable scope that performed the read, re-runs it, and then skips any child composables whose arguments are unchanged and stable. So a state change deep in a large screen can re-run just one small composable rather than the whole screen, provided the intervening composables are skippable. This is why keeping functions small and parameters stable localizes recomposition, and why reading state at the lowest possible level narrows the blast radius.

  28. Why can reading a state high in the tree cause the whole subtree to recompose, and how do you narrow the scope?

    If you read a frequently changing state in a high-level composable and then pass the derived value down, the read happens in the high-level scope, so that entire scope re-runs on every change, and although stable children may skip, everything that consumes the changed value must recompose. Narrowing the scope means moving the state read down to the smallest composable that needs it, or passing a lambda that reads the state so the read is deferred to a child or a later phase. For example, instead of reading a color state at the top and passing the color, pass a lambda returning the color and read it in the draw phase. This confines invalidation to exactly the code that depends on the value.

  29. What goes wrong when you use an unstable key with remember?

    remember with a key recomputes and discards its stored value whenever the key changes by equals comparison, so if the key is an unstable object that produces a new non-equal instance on each recomposition, the remembered value is thrown away and recreated every time, defeating the purpose of remembering. This can reset internal state, restart animations, or repeatedly run expensive initialization. The symptom is a value that never persists across recompositions even though you wrapped it in remember. The fix is to key on stable, value-equal inputs, such as an id or a primitive, rather than on a freshly allocated object.

  30. How should a ViewModel expose state to Compose, and what is the tradeoff between StateFlow and Compose State?

    A ViewModel typically exposes either a StateFlow collected with collectAsStateWithLifecycle or a Compose State via mutableStateOf read directly in the composable. StateFlow with collectAsStateWithLifecycle is lifecycle-aware, stops collecting when the UI is not visible, and interoperates with the coroutine world, making it the common recommendation for screen state. Holding mutableStateOf in the ViewModel is simpler and avoids a Flow, but you must ensure you are not leaking Compose types where they do not belong and that updates happen on the main thread. The key tradeoff is lifecycle-awareness and Flow interop versus directness; collectAsStateWithLifecycle is preferred over plain collectAsState because the latter keeps collecting even when the app is backgrounded.

  31. Why should you not create or remember a ViewModel with remember instead of the viewModel or hiltViewModel functions?

    The viewModel and hiltViewModel factory functions scope the instance to the correct ViewModelStoreOwner, usually the host Activity, Fragment, or a Navigation back stack entry, so the ViewModel survives configuration changes and is cleared at the right lifecycle boundary. Wrapping construction in remember only ties the instance to the composition, so it is recreated on configuration change and not cleared through onCleared, breaking the ViewModel contract and leaking coroutines. It also bypasses the store that lets the same ViewModel be shared across recompositions and navigation. Always obtain ViewModels through the lifecycle-aware provider, not remember.

  32. In Navigation Compose, where should screen state live, and what is hoisted versus retained?

    Navigation destinations should be stateless composables that receive their state and callbacks, with the durable state held in a ViewModel scoped to the back stack entry so it survives while that entry is on the stack and is cleared when the entry is popped. The NavController owns navigation state, the back stack, and current destination, which itself should be hoisted or held at the NavHost level rather than inside individual screens. Arguments are passed through the route and read from the back stack entry's arguments, not by passing objects directly, because the back stack must be able to recreate destinations. This keeps each screen testable in isolation and lets the framework restore the stack after process death.

  33. How are arguments passed and typed in Navigation Compose, and why avoid passing complex objects?

    Arguments are encoded into the route string as path or query parameters with declared NavType, then read back from the destination's arguments bundle, and newer type-safe navigation uses serializable route classes to declare argument types at compile time. You avoid passing large or complex objects because the back stack must survive process death by serializing arguments into a Bundle, and passing whole domain objects bloats saved state and can exceed Bundle limits. The recommended pattern is to pass only identifiers and let the destination's ViewModel load the full data from a repository. This keeps navigation state small, restorable, and decoupled from object lifetimes.

  34. How do you read recomposition counts to find performance problems, and what does a high count mean?

    Android Studio's Layout Inspector shows per-composable recomposition and skip counts while the app runs, letting you see which composables recompose far more often than expected. A high recomposition count on a composable that should be static usually points to an unstable parameter, a lambda that is reallocated each frame, or a state read placed too high in the tree. A high skip count next to a high recomposition count is healthier because it means Compose is avoiding work, whereas many recompositions with few skips indicates broken skipping. You use these numbers to locate the offending composable, then inspect its parameters' stability and where its state is read.

  35. What do the Compose compiler metrics tell you, and how do you act on them?

    The Compose compiler can emit reports listing each composable as restartable, skippable, or neither, and each class as stable, unstable, or immutable, along with why. You act on them by finding composables marked unskippable and tracing the unstable parameter responsible, then either making that type stable, adding an Immutable or Stable annotation where the promise holds, wrapping external types, or configuring a stability configuration file for classes you cannot annotate. This turns vague performance intuition into a concrete list of stability defects to fix. It is the authoritative way to know why a given composable is not being skipped, rather than guessing from runtime counts alone.

  36. In Compose UI tests, why do you often need waitForIdle or to control mainClock, and what does autoAdvance do?

    The test framework synchronizes assertions with Compose's work, and waitForIdle blocks until there are no pending recompositions, layouts, or idling resources so your assertion sees a settled UI. For animations and time-based effects, the test's mainClock lets you deterministically advance the virtual clock rather than waiting real time. autoAdvance, on by default, makes the clock advance automatically so animations run to completion during idle waits; setting autoAdvance to false lets you manually step the clock frame by frame to assert intermediate animation states. You disable autoAdvance when you need to inspect an animation mid-flight or avoid the test hanging on an indefinite animation.

  37. How do finders like onNodeWithText work, and why is the semantics tree, not the visual tree, what tests query?

    Compose tests query the semantics tree, a parallel tree of accessibility and testing metadata that composables publish through semantics modifiers, so onNodeWithText matches nodes whose semantics contain that text rather than scanning pixels. This means a component with no semantics, like a bare Canvas drawing text, is invisible to the finder unless you add a contentDescription or testTag. Querying semantics ties tests to the same information assistive technologies use, which encourages accessible components. When a node cannot be found, the usual cause is missing or merged semantics, and testTag or mergeDescendants behavior is what you adjust.

  38. What is the risk of doing heavy work directly in the body of a composable, and where should it go instead?

    The composable body can run many times per second during recomposition, so any expensive computation, allocation, sorting, or I/O placed directly in it repeats on every recomposition and can jank the frame. Pure derivations of parameters should be wrapped in remember keyed on their inputs so they recompute only when inputs change, and results derived from frequently changing state should use derivedStateOf. Genuine side effects, async work, or I/O must go into effect handlers like LaunchedEffect or into the ViewModel, never inline in composition. The mental model is that the composable body should be cheap, idempotent, and free of side effects.

  39. Why must composable functions be free of side effects and safe to run in any order or skip entirely?

    Compose may execute composables in parallel, run them in any order, skip them, or restart them, so a composable that mutates external state during composition can produce nondeterministic, corrupted results. The runtime treats composition as a pure description of UI that it can recompute freely, which is what enables skipping and recomposition optimizations. Side effects must therefore be funneled through the effect APIs, which the runtime schedules at well-defined, committed points rather than during the speculative composition pass. Violating this, for example incrementing a counter in the body, leads to bugs that appear only under recomposition timing you cannot control.

  40. What is the difference between collectAsState and collectAsStateWithLifecycle, and when does it matter?

    collectAsState collects a Flow into Compose State but keeps collecting as long as the composition is active, even when the app is in the background or the screen is not visible. collectAsStateWithLifecycle ties collection to a Lifecycle, pausing collection when the owner drops below the STARTED state and resuming when it returns, which avoids doing work and holding upstream resources for an off-screen screen. It matters most for hot flows backed by expensive sources like location, sensors, or network subscriptions, where background collection wastes battery and data. For app UI state on Android, collectAsStateWithLifecycle is the recommended default.

  41. How does interop work when embedding a Compose UI in a View hierarchy and vice versa, and what lifecycle concern arises?

    A ComposeView hosts Compose inside a View hierarchy and you must set an appropriate composition strategy so the composition is disposed at the right time, and you set its content with setContent. Conversely, AndroidView embeds a traditional View inside Compose, giving a factory to create the view once and an update block that runs on recomposition to sync Compose state into the view. The key lifecycle concern is that a ComposeView in a RecyclerView or a Fragment must use a ViewCompositionStrategy that disposes the composition appropriately, otherwise compositions leak or outlive their host. AndroidView's factory should be side-effect-light and the update block should carry the reactive bridge.

  42. Why can AndroidView's update block over-run, and how do you keep it efficient?

    The update block of AndroidView runs on the initial composition and again on every recomposition where any state it reads has changed, so if it reads several states or is placed under a frequently recomposing parent it re-executes often, pushing redundant work into the wrapped View. To keep it efficient, only read the specific states that should drive view updates, and let Compose's snapshot tracking limit re-execution to actual changes rather than doing unconditional work each pass. Avoid creating new objects in the block since the factory should own one-time setup. Treat update as a targeted synchronization point, not a general callback.

  43. What does it mean that Compose animations are state-driven, and how does this affect recomposition cost?

    Animation APIs like animateFloatAsState and Animatable expose the animating value as Compose State, so reading that value in composition causes a recomposition on every animation frame, which for a whole subtree can be costly. The mitigation is to read the animated value at the latest possible phase, for instance by driving graphicsLayer or an offset lambda with it so only draw or layout updates each frame. Transition-based APIs and the lambda-based modifier overloads are designed to keep per-frame work off the composition phase. Understanding this is why a naively animated large composable janks while a phase-aware one is smooth despite identical visuals.

  44. Why might a Composable that reads a MutableState still not recompose when you expect it to?

    The most common cause is mutating an object held inside the state rather than assigning a new value, since Compose only observes writes to the MutableState's value slot, not internal mutation of a non-observable object it holds. Another cause is that the read happens outside a recomposition scope, for example captured once in a remembered lambda or read in a background thread outside the snapshot system. A third is comparing with a structural equality policy where the new value is considered equal to the old, so no invalidation is scheduled. The fix depends on the cause: assign a new immutable value, use an observable collection type, or choose the correct SnapshotMutationPolicy.

  45. What is the difference between structural and referential equality policies for mutableStateOf, and when would you change it?

    By default mutableStateOf uses structural equality, so writing a value that equals the current one by equals schedules no recomposition, which avoids redundant work. You can supply referentialEqualityPolicy so any new reference, even an equal one, triggers invalidation, or neverEqualPolicy so every write always invalidates regardless of equality. You would change from the default when equality checks are expensive or misleading, or when you specifically need to force recomposition on every assignment such as replaying an identical event. Choosing the wrong policy either suppresses needed updates or causes spurious recompositions from equal writes.

  46. How does a custom Saver enable rememberSaveable for a non-trivial type, and what are the two directions it defines?

    A Saver defines how to convert your type into something the saved instance state Bundle can store and how to reconstruct it back, via a save function that returns a Bundle-compatible representation and a restore function that rebuilds the object from it. rememberSaveable calls save when the state is being preserved across configuration change or process death and restore when recreating the composable, so both directions must round-trip correctly. Convenience builders like listSaver and mapSaver reduce boilerplate by letting you express the object as a list or map of savable primitives. Without a Saver, rememberSaveable can only handle types that are already Bundle-compatible and will throw for arbitrary classes.

  47. Why can a lambda captured in a remember block become stale, and how does that differ from rememberUpdatedState?

    If you store a lambda with remember and no keys, it is computed once and never updated, so it keeps closing over the values that existed at first composition and will call stale data even as the composable receives new parameters. This differs from rememberUpdatedState, which deliberately updates its held value on every recomposition so long-lived consumers see the latest version. The distinction is that plain remember freezes both the object and its captures, whereas rememberUpdatedState refreshes the captured value each recomposition without changing the identity read by an ongoing effect. Choosing plain remember for a callback that must reflect current state is a subtle correctness bug.

  48. What is donut-hole skipping, and how does passing content lambdas enable it?

    Donut-hole skipping is the pattern where a composable that itself recomposes frequently accepts a content lambda whose composition was created in a scope that did not change, so the runtime recomposes the outer layer but reuses the inner content without recomposing it. Because a content lambda captures the composition context of its call site, if the data feeding that content did not change, the inner composables can be skipped even though their parent re-ran. This is why wrapping a frequently updating container around stable content, and passing that content as a slot, keeps the expensive inner UI from recomposing. It relies on the content lambda being stable and its captured inputs unchanged.

  49. Why does deferring a state read into a lambda passed to a child sometimes eliminate recomposition of the parent entirely?

    When the parent reads a state directly, the read is recorded in the parent's recomposition scope, so a change invalidates the parent. If instead the parent passes a lambda that performs the read and hands it to a child that invokes it during layout or draw, the actual read is attributed to the child's later phase, not the parent's composition, so the parent is never invalidated by that state. This moves the invalidation boundary down and possibly into a non-composition phase, converting an expensive parent recomposition into a cheap child layout or draw. It is the general principle behind the lambda overloads of offset, graphicsLayer, and drawBehind.

  50. How do the snapshot system's isolation and atomicity let Compose run composition off the main thread safely?

    Compose state lives in a snapshot system modeled on multiversion concurrency control, where each thread reads from a consistent snapshot and writes go to a private copy that is only published atomically on apply, with conflict detection between snapshots. This isolation means a recomposition running on a background thread sees a stable view of state that cannot be torn by concurrent writes, and its speculative changes are discarded if the composition is abandoned. Atomic apply ensures other observers never see a half-updated set of related states, preserving invariants across multiple MutableState objects. This machinery is what makes parallel and offloaded composition, retries, and safe recomposition possible without manual locking.

Practice all Jetpack Compose questions interactively

Search, filter, and mark questions complete in the free Preparation Path. You can start immediately without an account.

Open free Preparation Path

More Android interview topics