Articles
Jetpack Compose Interview Questions: Recomposition, Effect Keys, and Strong Skipping
Prepare for Jetpack Compose interview questions with effect-key scenarios, strong skipping comparisons, list identity, and callback lifetime exercises.
Jetpack Compose interview questions often hide three separate decisions inside the word “recomposition”: whether a scope needs work, whether an eligible composable body can be skipped, and whether an existing effect should restart. A parent running again does not automatically restart every child coroutine. A stable list key does not guarantee that a row body never executes.
Start by identifying the composition instance, then compare its inputs, then inspect the effect's keys. This guide applies that sequence to an order list and a delayed callback. The rules are drawn from official Android documentation checked September 11, 2026. The exercises are documentation-derived predictions, not recorded Android device traces.
What Exactly Is Being Recomputed?
Composition creates and maintains the description of the UI. Recomposition updates affected portions when observed inputs change. Skipping avoids executing an eligible composable body when its relevant inputs permit reuse. An effect has its own lifetime within that composition structure.
Imagine an order screen with a changing toolbar counter and three order rows. Updating the counter may cause its reader to run again. Whether each row body runs depends on where state is read, parameter comparison, compiler behavior, and any invalidation inside the row. Whether an order-loading coroutine restarts depends on the identity of its composition instance and its effect keys.
In an interview, replace “everything recomposes” with a specific prediction: “The parent reads the counter, so that scope is affected. The row receives the same order object. Its effect still has the same order ID.” This gives the interviewer assumptions to challenge rather than a slogan to accept.
Do not launch a network request directly in the composable body to observe this behavior. Body execution is not a once-only event contract. Put composition-bound work in an appropriate effect, and keep user-triggered operations in an event handler or state holder with the intended lifetime.
What Does Strong Skipping Change?
Google's strong-skipping documentation states that the mode is enabled by default from Kotlin 2.0.20. It makes restartable composables skippable even with unstable parameters. Non-restartable composables remain outside that change.
The comparison differs by parameter stability: stable parameters use equality; unstable parameters use instance identity. The compiler also memoizes lambdas under strong skipping, using their captures as keys. These rules do not mean that any function with unchanged-looking data will always be skipped.
For the following exercise, assume strong skipping is enabled, the row is restartable, its other parameters are unchanged, and it has no independently invalidated state read. Those assumptions deliberately isolate parameter comparison. In an actual project, record the Kotlin compiler and Compose compiler plugin versions and inspect compiler reports rather than inferring stability from a class name.
Suppose the row receives an unstable order model. Passing the very same instance on the next parent execution can permit skipping. Passing a freshly allocated instance with equivalent contents changes identity and can cause the row body to run. If a stable model has equal contents, equality can permit skipping even when the instance is new.
The word “can” matters. An interview prompt may add a changed callback, a state read inside the row, or an explicit non-skippable annotation. Revisit the assumptions instead of applying the matrix mechanically.
Predict an Order Row Before Optimizing It
Use this small model as a reasoning exercise:
class MutableOrder(val id: String, var status: String)
@Composable
fun OrderRow(order: MutableOrder) {
Text(order.status)
}
A normal mutable property is not automatically observable Compose state. Mutating status in place is therefore a poor basis for expecting a UI update. An unrelated parent update may once have made such code appear to work, but relying on that incidental execution is fragile.
The repair begins with state ownership. Publish an observable state change, often by replacing a value in a state holder with an immutable representation. Do not add @Stable to silence a report unless the type actually satisfies the stability contract. An annotation is a promise to the compiler, not a mechanism that turns arbitrary mutation into notifications.
| Change in the isolated exercise | Comparison or identity question | Expected reasoning |
|---|---|---|
| Same unstable object passed again | Same instance? Yes | Row may be skipped under the stated assumptions. |
| Equal-looking new unstable object | Same instance? No | Parameter change prevents that skip. |
| New stable value equal to old value | Equality unchanged? Yes | A new allocation alone need not force execution. |
| Ordinary mutable field changes in place | Was observable state written? | Do not assume the UI is notified. |
| State read inside the row changes | Is the row itself invalidated? | Parent parameter equality is insufficient to conclude no work. |
A useful follow-up is to predict the visible bug, not merely count executions. If an order changes from Pending to Shipped but the UI stays Pending, identify the missing observable update. A lower recomposition count is not a success if the displayed state is wrong.
How Do Effect Keys Define the Work Lifetime?
The official side-effects guide describes LaunchedEffect as composition-bound coroutine work that restarts when its keys change. Leaving composition cancels that work. A key should therefore represent something whose change makes the old operation obsolete.
For an order detail request, an order ID is a plausible key:
LaunchedEffect(order.id) {
loadOrder(order.id)
}
This is an illustrative fragment, not a complete repository or error-handling design. If the selected ID changes from A to B while loading A, the old composition-bound operation is cancelled and new work begins for B. If another property changes while the ID stays A, that key alone does not request a restart.
Now change the requirement: a refresh token should force reloading the same order. The effect needs to represent that additional lifetime input, or refresh should be modeled through another deliberate owner. Including every variable indiscriminately is not a solution; a volatile value can repeatedly cancel useful work.
Ask what cancellation actually protects. It can stop cooperative coroutine work, but it cannot guarantee that an external service never received a request or undo a completed side effect. Network and repository design still need appropriate handling of stale results and duplicate operations.
How Can a Callback Change Without Resetting a Timer?
Consider a confirmation hint that waits two seconds before notifying its caller. At the start, callback A is supplied. After half a second, the parent supplies callback B. The product requirement is to preserve the original deadline and invoke the latest callback.
A constant-key effect that directly captures the original callback does not express that requirement safely. Adding the callback as a key expresses a different requirement: restart the timer when that callback changes. The documented rememberUpdatedState pattern separates the deadline's lifetime from the value read when it completes.
@Composable
fun ConfirmationHint(onElapsed: () -> Unit) {
val latestOnElapsed by rememberUpdatedState(onElapsed)
LaunchedEffect(Unit) {
delay(2_000)
latestOnElapsed()
}
}
This example follows the lifetime of this composition instance. Unit is not a promise to execute once for the entire application. Removing and later recreating the hint creates a new lifetime. If the hint should follow an order instead, include that identity in the design.
The following timeline is a conceptual prediction. It assumes callback B is a changed key where used, the instance remains composed, and scheduling delay is ignored. It is not a millisecond-accurate performance measurement.
| Design | At 500 ms: callback becomes B | At the planned completion |
|---|---|---|
| Constant key, original callback captured | Existing timer remains | Original callback A can be used. |
| Callback used as effect key | Old timer cancelled; new timer starts | B runs around 2,500 ms in this idealized model. |
| Constant key plus updated-state read | Existing timer remains; latest reference updates | B runs around 2,000 ms in this idealized model. |
Do not read the updated value into a separate local variable before the delay and then invoke that captured value later. The intended pattern reads the state when the callback is needed. Conversely, if the operation genuinely belongs to the original callback or request, deliberately capturing it can be correct. State the requirement first.
What Happens When the Order List Is Reordered?
Suppose the screen initially renders A, B, C and then renders C, A, B. The official lifecycle documentation explains how keys let Compose associate repeated calls with their logical items rather than relying only on execution position.
Column {
orders.forEach { order ->
key(order.id) {
OrderRow(order)
}
}
}
The IDs must distinguish siblings at that call site. Reusing the same ID for different orders removes the distinction you intended to provide. A random key generated during every execution is also counterproductive: it says that the identity changed even when the business item did not.
In a lazy list, provide the item key through the corresponding items API. Keep the distinction between the list's item identity and a nested effect's restart keys. One identifies the composition instance; the other identifies the work within it.
For an original practice trace, imagine each row logs its ID when an effect starts and when its coroutine is cancelled. With retained keyed rows and unchanged effect IDs, reordering alone should not require cancelling A's work and starting it as C's work. With position-based association, changed row inputs can cause effects to be replaced as items move.
Avoid promising a universal exact log order across rows. Scheduling, disposal, visibility in a lazy list, and additional state changes can alter the sequence. The invariant to investigate is whether work stayed associated with the intended order, not whether every debug line appeared in a memorized order.
How Would You Verify These Predictions?
Build a minimal screen with explicit buttons for three changes: reorder existing instances, replace an instance with equivalent data, and replace the completion callback. Record compiler settings, use unique order IDs, and ensure only one condition changes per experiment.
Instrument lifecycle events rather than treating a print in the composable body as proof of a committed UI change. For the timer, use a controlled test clock and assert which callback receives the event. For the list, assert that each visible row and its state still belong to the same order after reordering.
This article has not run those Android instrumented tests. Its predictions are a preparation checklist derived from documented behavior; verify them against your app's pinned toolchain. Do not substitute a standalone script that compares predefined booleans for a Compose runtime test.
If the interview turns to performance, collect a relevant trace or benchmark on an appropriate build and device. Recomposition counts alone do not establish jank or improvement. A body can execute cheaply while layout, decoding, or main-thread work dominates the user-visible delay.
Practice Explaining State and Lifetime
| Question | What to explain |
|---|---|
| Build a Compose Rating Card | Separate state, validation, submission, and success UI. |
| Design Android MVVM API Architecture | Name the owner of asynchronous work. |
| Design a Photo Album App | Preserve item identity as collections change. |
| Design Mobile Model Usage Quotas | Distinguish screen lifetime from persisted policy. |
| Build a Streaming Chat Input | Explain cancellation and updates during ongoing work. |
Choose one Android architecture practice question, name the state owner, and predict one reorder or callback change before coding. Then explain which observation would prove your prediction wrong.
Sources and Further Reading
- Strong skipping mode — compiler behavior and version boundary.
- Side-effects in Compose — effect lifetime, keys, and updated callbacks.
- Lifecycle of composables — call identity and list keys.
- State and Jetpack Compose — observable state and state hoisting.
- Compose performance — measuring UI performance.