Android · 14 min read

Jetpack Compose interview questions and follow-ups

Compose interview questions on recomposition, state, side effects and performance, with the weak answer, the answer that scores and the follow-up.

This page is for Android engineers with a Compose-first loop coming up: a phone screen, a technical round, or a live coding task built in Compose. You get twelve Jetpack Compose interview questions as they are asked, what each one scores, the weak answer, the answer that scores, and the follow-up that decides it. Every Compose question in an interview is a lifetime question in disguise. The API name is the entry fee.

What a Compose question is really scoring

When I ask about Compose, I am listening for one thing: does the candidate know who owns each piece of state, and how long it lives? Composition, recreation, process death and the database are four different lifetimes. A candidate who keeps them apart can recover from a forgotten API name. A candidate who blurs them usually cannot, however fluent the vocabulary.

Signal What I write down What a miss sounds like
Lifetime Names what survives recomposition, rotation and process death, separately “remember keeps it”
Evidence Measures before fixing: recomposition counts, compiler reports, traces “I would add remember everywhere”
Currency Knows the rules that changed, and when Stability advice from before strong skipping
Ownership One state owner per screen, state hoisted only as high as needed Everything in the ViewModel, or nothing

State and restoration

1. “What is the difference between remember and rememberSaveable?”

What is scored: whether you separate recomposition from recreation.

Weak answer: “I use rememberSaveable everywhere because it is the safe version.”

The answer that scores: “remember survives recomposition only. It dies with the composition, so it is gone after rotation and after process death. rememberSaveable writes into the saved instance state, so it survives both, within the size limits of that bundle. I use it for small UI values: a draft in a text field, a selected tab. Anything large, or anything with a real source of truth, belongs in a ViewModel with SavedStateHandle or in the database.”

The trap: testing only rotation. remember fails there too, so the candidate concludes rememberSaveable is a rotation fix and never tests process death.

The follow-up: “The value is a large domain object that lives in Room. Why serialise it into the bundle?” You should not. Save the id, reload the object from its owner.

Recovery line: “Let me be more precise: I would save only the minimal input needed to rebuild the screen, and reload durable data from where it lives.”

2. “A screen loses its scroll position and form input on rotation, although the state uses remember. Why?”

What is scored: the same model under a symptom.

The answer that scores: “Rotation recreates the activity, so the composition is destroyed and rebuilt, and remembered state goes with it. Scroll position comes back with rememberLazyListState, which is saveable internally. Text with a selection can use rememberSaveable with TextFieldValue.Saver. Screen state moves to the ViewModel, which survives the configuration change but not process death.”

The trap: fixing it with android:configChanges. It hides the bug and breaks resource reloading.

Recomposition and performance

3. “A colleague claims every state change recomposes the entire screen. Correct them.”

What is scored: a precise model of invalidation.

The answer that scores: “A write to snapshot state invalidates the scopes that read that state. Those scopes re-execute on the next frame. A child with the same inputs as last time may be skipped. Reads during layout or drawing belong to those phases, so reading state inside a draw lambda triggers no recomposition. The whole screen runs again only if the whole screen reads the value, which is usually the actual bug.”

The follow-up: “Only one leaf read the changed value. Why did a sibling run again?” Now you talk about parameters the runtime could not treat as unchanged, or lambdas recreated with new captures.

4. “How do you stop unnecessary recompositions in a complex screen?”

What is scored: measurement before technique, and current rules.

Weak answer: “Annotate everything with @Stable and wrap all the lists in ImmutableList.”

The answer that scores: “I measure first: Layout Inspector recomposition counts to find the scope, Compose compiler reports to see what the compiler inferred. Then I narrow the read to the smallest composable that needs it, defer reads into lambdas where the value is only needed at layout or draw time, keep keys stable in lazy lists, and use immutable UI models. Strong skipping is the default since Kotlin 2.0.20: composables with unstable parameters can still skip when the arguments are the same instances, and lambdas are remembered for you. So much of the older stability folklore no longer applies.”

The trap: reciting pre-2.0.20 advice as current. It tells the interviewer your material has an expiry date that already passed.

The follow-up: “How would you prove it recomposed less?” Name the tool and the before and after: recomposition counts in Layout Inspector for the scope, and frame timing with Macrobenchmark if the claim is about jank. Profile a release build with R8 on. Debug Compose runs without those optimisations, so its numbers point you at slowness that does not ship.

5. “derivedStateOf or remember with keys?”

What is scored: knowing they solve different problems.

The answer that scores: “remember(keys) caches a calculation and recomputes when a key changes. It fits when input and output change together, like filtering a list by a query. derivedStateOf fits when the input changes far more often than the result: the scroll offset changes every frame, but ‘show the scroll-to-top button’ flips twice. Only a change in the derived result invalidates whoever reads it. It has an observation cost, so using it where keys would do is a net loss. And it has to be wrapped in remember, or it is rebuilt on every pass.”

The follow-up: “Does derivedStateOf make the calculation run less often?” Not necessarily. It still observes every input change. It reduces invalidation of readers, not the work of the calculation. A candidate who corrects that overclaim scores well.

6. “After a refresh, the list flashes and the scroll position jumps.”

What is scored: separating a UI identity bug from a data bug.

The answer that scores: “First the keys. If items are keyed by position, or not keyed, an insertion changes every item’s identity and Compose cannot keep the right state with the right row. Items need stable domain ids as keys. Then the data: if a paging refresh replaces rows and remote keys in two steps, the UI can observe a half-updated database. That write should be one transaction. I would check each cause on its own.”

Collecting state

7. “How do you collect a StateFlow in a composable?”

What is scored: lifecycle awareness on both ends.

The answer that scores: “With collectAsStateWithLifecycle, which collects only while the lifecycle is at least started. Plain collectAsState keeps collecting in the background. On the ViewModel side, stateIn with SharingStarted.WhileSubscribed(5_000), so the upstream stops too when nobody is looking, and survives a rotation without restarting.”

class InboxViewModel(messages: MessageRepository) : ViewModel() {
    val uiState: StateFlow<InboxState> = messages.observeInbox()
        .map { InboxState.Ready(it) }
        .stateIn(viewModelScope, SharingStarted.WhileSubscribed(5_000), InboxState.Loading)
}

@Composable
fun InboxRoute(vm: InboxViewModel) {
    val uiState by vm.uiState.collectAsStateWithLifecycle()
    InboxScreen(uiState)
}

The trap: WhileSubscribed(0). Every rotation restarts the expensive upstream.

The follow-up: “Why expose transformed state instead of the repository flow?” One owner does the mapping, the UI renders, and the mapping is testable without a composable.

Side effects

8. “What are the common mistakes with LaunchedEffect, DisposableEffect and rememberCoroutineScope?”

What is scored: choosing the API from the lifetime, not the name.

The answer that scores: “LaunchedEffect is for suspend work tied to the composition, restarted when a key changes. DisposableEffect is for a registration that needs a matching cleanup in onDispose. rememberCoroutineScope is for work started by a user event, from the click handler. The mistakes follow from mixing them: wrong keys, so the effect restarts too often or captures a stale callback; LaunchedEffect(someState) used as an event handler; a listener registered with no disposal. In production that shows up as duplicate analytics, leaked listeners and snackbars that fire again on rotation.”

The follow-up: “The callback changes while the Unit-keyed effect keeps running. Which callback does it call?” The old one, unless you read it through rememberUpdatedState.

9. “You register a listener when a composable appears. Where does the unregister go?”

The answer that scores: “In DisposableEffect, keyed by the resource it depends on, with the unregister in onDispose. Cancellation stops a coroutine. It does not remove a listener from an API that knows nothing about coroutines.”

The trap: keying the effect on Unit while the listener belongs to an id that changes. The effect runs once, and the new id never gets its own registration.

10. “The screen-viewed analytics event fires three times per visit. Why?”

What is scored: knowing a composable body is not a lifecycle callback.

The answer that scores: “The call is in the body, and the body runs on every recomposition. It belongs in an effect keyed by what the product means by a visit, or at the navigation boundary. LaunchedEffect(Unit) means once per entry into composition, not once per visit. So first I would ask whether a new navigation entry, or a restore after process death, counts as another visit.”

Ownership and events

11. “This composable owns its own state and cannot be reused or tested. Refactor it.”

The answer that scores: “State hoisting: replace the internal state with a value parameter and an event callback, so the caller owns it. The component becomes stateless, previewable and testable with plain inputs. I hoist only to the lowest common owner. A row’s expanded flag does not belong in the screen ViewModel. If call sites get noisy, I keep a stateful wrapper around the stateless core.”

The follow-up: “The expanded state belongs to one reusable row. What requires the ViewModel to own it?” Nothing does. Saying so is the judgement signal.

12. “How do you show a snackbar or navigate once, from the ViewModel?”

What is scored: delivery guarantees, not a favourite type.

The answer that scores: “State is replayed to every new collector, so a navigation flag in a StateFlow fires again after rotation. I first ask whether the thing is state at all. Often it is: a message to show, which the UI marks as shown. That is the pattern the Android documentation describes. For fire-and-forget effects, a Channel collected in a LaunchedEffect works, with the known cost that an event emitted while nobody collects can be lost. For a payment result, losing it is not acceptable, so it becomes state with an id.”

Recovery line: “I should define whether losing this event is acceptable first. Then the type follows.”

Worked example: the search screen

This is the exchange I would run with a candidate, condensed.

Prompt: “Scrolling makes the search screen recompose constantly, and typing lags.”

Strong opening: “I would not start by adding remember. I would find which state is read by which scope, and measure. Often the culprit is a composable near the root reading the raw scroll offset, when all the UI cares about is whether it crossed a threshold, so I would move that into derivedStateOf. Then stable item keys, and filtering moved out of the composable body or remembered by its real inputs. Then counts and a frame trace before I claim a fix.”

Push: “The list still flashes after refresh.” Keys first, then whether the refresh is one transaction, as in question 6.

Push: “Analytics says the screen-view fires three times.” Question 10.

How I would score it:

Score What the candidate did
Hire signal Separated invalidation, identity, data refresh and effect lifetime, and said how each claim is measured
Acceptable Named derivedStateOf, stable keys and the effect APIs, but overstated one guarantee
Mid-level Reached for remember, derivedStateOf and a Unit-keyed LaunchedEffect but could not say when each one starts or ends
Weak Blamed Compose and proposed rewriting the screen in Views before profiling

Interop and migration

A Compose loop at a company with an older app often ends here: “How would you migrate a large XML app to Compose?” The strong answer migrates screen-sized slices, starting with a representative leaf screen rather than the hardest custom View, keeps one state owner while both stacks coexist, shares design tokens so the two themes cannot drift, and protects parity with screenshot tests and restoration checks. The trap is choosing the hardest screen as the proof of concept and stalling there for a quarter. The follow-up is ownership: “Which object holds state while one screen has both Views and Compose?” One owner, usually the existing ViewModel, and never a copy on each side.

Expect questions on the boundary in both directions. To host a View in Compose you use AndroidView. factory creates the view once; update runs again whenever state it reads changes, so keep expensive work out of it. Some views need lifecycle calls forwarded: a MapView that never gets onResume and onDestroy renders blank and leaks. Compose inside a Fragment uses ComposeView, and the detail I listen for is ViewCompositionStrategy.DisposeOnViewTreeLifecycleDestroyed. Without it the composition is tied to the wrong lifecycle and leaks across view recreation, which is the Fragment’s two-lifecycle problem again.

How to use this list

Say your own answer out loud before you read the one that scores, then answer the follow-up without looking. The follow-up is where the round is decided. If your second answer is weaker than your first, that is the card to work on. If you want a book rather than a page, Android interview books compares the options by time left. The broader Android loop is in Android technical interview questions, and the language questions that sit under Compose are in Kotlin interview questions.

What changed in 2024 to 2026

  • Kotlin 2.0.0, released 21 May 2024, made the K2 compiler the default and Stable on JVM, Native, Wasm and JS (What’s new in Kotlin 2.0.0).
  • In the same release the Jetpack Compose compiler plugin moved into the Kotlin repository and gained its own Gradle plugin.
  • Answers about build speed or compiler plugins that describe the old compiler describe a toolchain most projects have left.

Sources

What to do next

Free guide: the Phoenix Method