Skip to content
Mobile & design

11 Android Views and Compose Interview Questions and Answers

This focused guide turns RecallDeck’s curated Android Views and Compose material into 11 interview-ready questions. Answer each one before opening the explanation, then use the examples and edge cases to repair anything vague or incomplete.

10 min read11 detailed answersReviewed Aug 24, 2026
What to remember

Tie implementation or design choices to platform behavior, user feedback, lifecycle, accessibility, and the trade-off the team must maintain.

Question set

11 detailed answers

01

What is recomposition and what triggers it?

Short answer: Recomposition is re-invoking @Composable functions when the state they read changes. It is triggered by a write to an observable State (mutableStateOf, a StateFlow via collectAsState, etc.) that was read inside the composable. Only the functions that read that particular value re-run.

In depth:

@Composable
fun Counter() {
    var count by remember { mutableStateOf(0) }
    Text("Clicks: $count")           // reads count → re-invoked
    Button(onClick = { count++ }) {  // writes count → recomposition
        Text("Tap")                  // doesn't read count → skipped
    }
}
  1. The trigger is not "any change" — it is a write to snapshot state that was read in a specific composable. Compose tracks reads and invalidates exactly the affected scopes.
  2. Optimistic — if state changes again mid-recomposition, the current pass is discarded and restarted.
  3. Unordered and possibly parallel — the execution order of sibling composables is not guaranteed; don't rely on side effects in the body.

⚠️ Common mistake: a plain var without mutableStateOf is not state; Compose can't see its change and won't recompose — the UI freezes.

02

remember vs rememberSaveable — what's the difference?

Short answer: remember caches a value across recompositions within one composition; on recreation (rotation, process death) it is lost. rememberSaveable additionally saves the value into a Bundle (savedInstanceState) and restores it after a configuration change or process recreation.

In depth:

remember rememberSaveable
Survives recomposition yes yes
Survives rotation / config change no yes
Survives process death no yes (via Bundle)
What it can hold anything Bundle-compatible or a custom Saver
  1. rememberSaveable — for what the user entered: field text, selected tab, position.
  2. Constraint — the value must fit a Bundle: primitives, Parcelable, or a custom Saver/listSaver/mapSaver.
  3. Recreation keyremember(key) { }: changing the key recreates the value.

⚠️ Common mistake: stashing a large object (a network list) in rememberSaveable — a Bundle is capped near ~1 MB (TransactionTooLargeException). Cache heavy data in a ViewModel and keep only ids/position in saveable.

03

What is state hoisting and unidirectional data flow?

Short answer: State hoisting moves state out of a composable up to its caller, making the composable stateless: it takes a value plus an onValueChange callback. That is unidirectional data flow (UDF): state flows down, events flow up.

In depth:

// stateless: state hoisted to the caller
@Composable
fun NameField(name: String, onNameChange: (String) -> Unit) {
    TextField(value = name, onValueChange = onNameChange)
}

// the state owner sits higher up (often a ViewModel)
@Composable
fun Screen(vm: ProfileVM) {
    val name by vm.name.collectAsStateWithLifecycle()
    NameField(name = name, onNameChange = vm::onNameChange)
}
  1. State down, events up — the composable doesn't own what it draws; it's a pure function of its parameters.
  2. Why — reusability, testability, a single source of truth, easy @Preview.
  3. How high to hoist — to the lowest common parent that reads the state; for screen state, usually the ViewModel.

⚠️ Common mistake: hoisting too high (all screen state in one top-level mutableStateOf) — any tiny change recomposes the whole subtree. Keep state at the lowest level that needs it.

04

What makes a composable skippable, and how do stability and strong skipping relate?

Short answer: Compose skips a composable's recomposition when all its parameters are stable and unchanged by equals(). A type is stable when Compose is sure its public properties are immutable or themselves observable. An unstable parameter (List, a type from another module) used to make a composable non-skippable. Strong skipping (default since Kotlin 2.0.20) changes that.

In depth:

Parameters Before strong skipping With strong skipping
All stable, unchanged skip (by equals) skip (by equals)
Unstable (List, foreign class) never skips skips if same reference (===)
Unstable lambda parameter broke skipping memoized, doesn't break it
  1. What's stable — primitives, String, function types, plus classes marked @Stable/@Immutable or provably immutable (all vals of stable types).
  2. Strong skipping doesn't make types stable — it changes the skip-check: unstable params are compared by reference (===), stable ones by equals().
  3. Lambda memoization — under strong skipping, even lambdas with unstable captures are wrapped in remember, so an unstable lambda no longer breaks skipping.

⚠️ Common mistake: assuming strong skipping "makes everything stable" so you can ignore stability. A fresh List on every call (a new reference) still forces recomposition — @Immutable wrappers and kotlinx.collections.immutable are still needed.

05

LaunchedEffect, DisposableEffect, rememberUpdatedState, snapshotFlow — which one when?

Short answer: LaunchedEffect runs suspend work tied to the composition, restarting on key change. DisposableEffect is for effects that need cleanup (onDispose): subscriptions, listeners. rememberUpdatedState captures the latest value inside a long-lived effect without restarting it. snapshotFlow turns State into a Flow so you can apply operators.

In depth:

API For what Key detail
LaunchedEffect(key) suspend on entering composition key change = cancel + restart
DisposableEffect(key) effect with cleanup onDispose {} is required
rememberUpdatedState(v) latest value without restart for long effects with a fixed key
snapshotFlow { } State → cold Flow then debounce/map/distinct
  1. LaunchedEffect keys — the effect restarts exactly when a passed key changes; LaunchedEffect(Unit) runs once for the composable's lifetime.
  2. rememberUpdatedState — the classic: an onTimeout inside LaunchedEffect(Unit) must see the latest callback, but the timer must not restart every time the callback changes.
  3. snapshotFlow — a bridge from Compose state to Flow: snapshotFlow { listState.firstVisibleItemIndex }.distinctUntilChanged().

⚠️ Common mistake: launching a coroutine/subscription directly in the composable body instead of an effect — it starts on every recomposition and leaks. Side effects belong only inside effect APIs.

06

Why set key and contentType on LazyColumn items?

Short answer: key gives an item a stable identity: on insert/remove/move Compose matches items by key instead of position, preserving their inner remember state and animating correctly. contentType tells the reuse machinery which items are "of the same type" so it can reuse their compositions more effectively.

In depth:

LazyColumn {
    items(
        items = messages,
        key = { it.id },            // stable identity
        contentType = { it.kind },  // text / image / divider
    ) { msg -> MessageRow(msg) }
}
  1. Without key — identity is the index: delete the top item and all remember state (expanded flag, input, inner scroll) slides onto the neighbors.
  2. key and performance — when the list changes Compose reuses already-built items instead of rebuilding them all.
  3. contentType — the lazy layout keeps a pool per type; with heterogeneous items (headers, cells), the same contentType lets slots be reused and a different one keeps incompatible items apart.

⚠️ Common mistake: the key must be stable and unique. Using the index as key is pointless (that's the default), and non-unique keys throw at runtime.

07

Why does modifier order change the result (padding, clickable, background)?

Short answer: A Modifier is an ordered chain; each modifier wraps whatever comes after it. Order affects size and padding, the drawing area, and the click area. padding before background isn't part of the background; padding before clickable shrinks the clickable region.

In depth:

// background is wider; click and ripple cover the whole filled area
Modifier.background(Blue).padding(16.dp).clickable { }
// vs
// padding is outside the background; click and fill only cover the inner rect
Modifier.padding(16.dp).background(Blue).clickable { }
Order Effect
backgroundpadding background wider; padding sits over the fill
paddingbackground padding outside, background narrower
paddingclickable click only inside the padding
clickablepadding click and ripple over the whole area, padding included
  1. The model — sizing modifiers apply down the chain during measure, and drawing follows the chain too; the "outer" one is written earlier.
  2. In practiceclickable is usually placed so the whole visual area (with finger-friendly padding) is tappable and the ripple covers the right region.

⚠️ Common mistake: treating Modifier as an unordered "bag of properties" like a CSS class. It is function composition: A.then(B) ≠ B.then(A).

08

How does a custom View take part in measure/layout/draw, and what's the onMeasure contract?

Short answer: A View goes through three passes: measure (onMeasure — determine the desired size from the parent's MeasureSpec), layout (onLayout — place children at coordinates), draw (onDraw — paint on the Canvas). In onMeasure you must call setMeasuredDimension, respecting the MeasureSpec mode.

In depth:

measure(widthSpec, heightSpec)      parent → child
   └─ onMeasure → setMeasuredDimension(w, h)
layout(l, t, r, b)
   └─ onLayout → child.layout(...) for each child
draw(canvas)
   └─ onDraw(canvas)   // no allocations!
  1. MeasureSpec — the parent passes mode + size: EXACTLY (exact size: match_parent/dp), AT_MOST (no bigger: wrap_content), UNSPECIFIED (whatever you want: scroll containers).
  2. onMeasure contract — compute your size and call setMeasuredDimension(w, h); resolveSize() helps honor the mode. Not calling it throws IllegalStateException.
  3. onLayout — only for a ViewGroup: call child.layout(l,t,r,b) for each child (coordinates relative to the parent).
  4. onDraw — no allocations here (Paint(), objects): it runs on every frame.

⚠️ Common mistake: ignoring MeasureSpec and always returning the desired size — under EXACTLY (match_parent) the view overflows or gets clipped; always check the mode.

09

How does RecyclerView recycling work, and why DiffUtil with payloads?

Short answer: RecyclerView keeps a minimum of ViewHolders on screen and reuses them: a holder scrolled off-screen goes into a pool, and when a new item appears it's pulled out and merely bind-ed with new data — inflate/findViewById happen rarely. DiffUtil/ListAdapter compute a minimal diff, and a payload lets you update a holder partially, without a full rebind.

In depth:

[off-screen] → RecycledViewPool → onBindViewHolder(newData) → [on-screen]
     ▲                                                            │
     └──────────────── scrolled off ─────────────────────────────┘
  1. ViewHolder pattern — caches view references (no findViewById on every bind); onCreateViewHolder is rare, onBindViewHolder frequent.
  2. Pools — Scrap (temporarily detached, returned without rebinding), Cache, RecycledViewPool (keyed by viewType, shareable between lists).
  3. DiffUtil / ListAdapterareItemsTheSame (same entity by id), areContentsTheSame (did the content change); on a change getChangePayload gives a partial update (e.g. only the like count), and onBindViewHolder(holder, pos, payloads) redraws just that.

⚠️ Common mistake: notifyDataSetChanged() on every update — it rebinds the whole list and loses animations. ListAdapter.submitList + DiffUtil compute the precise changes for you.

10

How does invalidate() differ from requestLayout()?

Short answer: invalidate() marks a view "needs redraw" — only the draw pass (onDraw) runs. requestLayout() says "my size may have changed" — measure + layout run up the branch, then draw. requestLayout is more expensive.

In depth:

invalidate() requestLayout()
What it triggers draw (onDraw) measure + layout + draw
When only appearance changed (color, same-length text) size/position changed
Scope the view itself up the hierarchy to the root
  1. invalidate — color, progress, an animation of the same size: repaint pixels.
  2. requestLayout — content that affects size changed (text got longer, a view shown/hidden): recompute geometry.
  3. Thread — both are called on the UI thread; from another thread use postInvalidate().

⚠️ Common mistake: calling requestLayout() where invalidate() suffices (you just changed a color) — an extra measure/layout of the whole branch every frame causes jank.

11

Why are deep View hierarchies slow, and how does ConstraintLayout help?

Short answer: Every nested ViewGroup adds measure/layout passes, and nested weights or RelativeLayout force children to be measured twice (double taxation) — in the worst case the number of measurements grows exponentially with depth. ConstraintLayout builds a flat hierarchy: complex layouts without nesting, usually in two passes.

In depth:

Deep (slow):                    Flat (ConstraintLayout):
Frame > Linear > Linear >       ConstraintLayout
  Relative > TextView             ├─ TextView  (constraints)
(each level = a pass)             └─ ImageView (constraints)
  1. Cost of depth — measure and layout are recursive; the deeper the tree, the more passes and the longer the frame (~16 ms budget at 60 fps).
  2. Double taxation — LinearLayout with weight and RelativeLayout measure children twice (first to learn, then to distribute); nesting such containers multiplies the measurements.
  3. ConstraintLayout — all constraints on one level, flat; removes nested LinearLayouts, usually 2 passes. Plus Barrier, Chain, and guidelines instead of weights.
  4. More tools<merge> (drop a redundant root ViewGroup), ViewStub (lazy inflate), fighting overdraw.

⚠️ Common mistake: fixing everything with nested LinearLayouts and layout_weight — that is the source of double taxation. A flat hierarchy is almost always faster than a deep "tidy" one.

Source notes

References and review policy

RecallDeck’s interview answers are editorial material, reviewed against maintained official documentation where a primary reference is available. Tool selections use direct provider links and contain no affiliate placements. Features can change after the review date.

From reading to recall

Practice the full interview loop.

RecallDeck schedules the concepts you miss and keeps coding, design, and behavioral fundamentals available when the interviewer changes direction.

Start studying

Keep going

RecallDeck Interview Library

Detailed answers from the same curated interview deck, organized for search, study, and durable recall.

RSS