A composable receives `data: List<User>` as a parameter and is never skipped during recomposition, even when the parent recomposes with an identical list reference. All properties of `User` are `val`. What is the MOST likely reason Compose treats this parameter as unstable?
- A. `List<User>` is an interface type that Compose cannot prove immutable, so it infers the parameter as unstable ✓
- B. Passing any `List` always forces a recomposition because collections are compared structurally, not by reference
- C. `User` must be annotated with `@Composable` for the compiler to consider it stable
- D. The composable is missing the `@Stable` annotation on its function declaration
Correct answer: A. `List` is an interface with no immutability guarantee, so the Compose compiler infers it unstable; using `ImmutableList`/`@Immutable` or a `kotlinx.collections.immutable` type fixes it.
Inside a `ViewModel` you write `viewModelScope.launch { val a = async { apiA() }; val b = async { apiB() }; combine(a.await(), b.await()) }`. `apiB()` throws. What happens to `apiA()`'s coroutine and how does the exception surface?
- A. `apiA()` keeps running to completion; the exception is silently swallowed by `async`
- B. `apiA()` is cancelled and the exception propagates to `viewModelScope`, cancelling the whole scope ✓
- C. Only the `async { apiB() }` fails; `a.await()` returns normally and the crash is deferred until `b.await()`
- D. Both `async` calls are cancelled but the exception is caught automatically by `viewModelScope`'s default handler
Correct answer: B. Because the `async` children share the `launch` job under structured concurrency, `apiB`'s failure cancels its siblings (including `apiA`) and propagates up, cancelling `viewModelScope`.
You expose one-time navigation events from a ViewModel using `val events = MutableStateFlow<Event?>(null)` collected in the UI with `collect { ... }`. After a config change (Activity recreated, ViewModel retained), users report the same navigation event fires again. What is the root cause?
- A. `MutableStateFlow` drops emissions, so the re-collection reads a stale buffered event by mistake
- B. `StateFlow` always replays its current value to new collectors, so the retained non-null event is re-delivered on re-subscription ✓
- C. `collect` on the main thread cannot receive events after a config change unless you use `collectLatest`
- D. `MutableStateFlow` requires `distinctUntilChanged` to be disabled to avoid duplicate events
Correct answer: B. `StateFlow` has replay-1 semantics and always emits its current value to new collectors, so retaining a non-null event re-fires it; a `SharedFlow` with replay 0 (or Channel) is the correct primitive for events.
Given `flow { emit(1); emit(2) }.map { heavyCpu(it) }.flowOn(Dispatchers.Default).collect { updateUi(it) }` collected from `Dispatchers.Main`, on which dispatcher does `heavyCpu` run and on which does `updateUi` run?
- A. Both `heavyCpu` and `updateUi` run on `Dispatchers.Default`
- B. `heavyCpu` runs on `Dispatchers.Default`; `updateUi` runs on `Dispatchers.Main` ✓
- C. Both run on `Dispatchers.Main` because `collect` dictates the whole chain's context
- D. `heavyCpu` runs on `Dispatchers.Main`; `updateUi` runs on `Dispatchers.Default`
Correct answer: B. `flowOn` changes context only for upstream operators (the `map` and the source), while the collector runs in the collecting coroutine's context (`Main`).
A `LaunchedEffect(Unit) { while(true) { delay(1000); tick() } }` is placed inside an `if (isVisible)` block. A colleague changes it to `LaunchedEffect(isVisible)` and moves it outside the `if`. What behavioral difference results when `isVisible` toggles true→false→true?
- A. The keyed version restarts `tick()` cleanly on each toggle; the original leaves the coroutine running while invisible ✓
- B. Both versions behave identically because `LaunchedEffect` is keyed by composition position anyway
- C. The keyed version never restarts because `isVisible` is a Boolean, not a stable key
- D. The original version keeps `tick()` running forever even when removed from composition
Correct answer: A. Placing `LaunchedEffect` inside the `if` cancels it when the block leaves composition; keying on `isVisible` outside keeps it in composition but cancels/relaunches whenever the key changes.
Consider: `class Analytics(private val activity: Activity)` stored in a `companion object`-held singleton created from `onCreate`. LeakCanary flags the Activity. What reference chain keeps the Activity alive across rotation?
- A. The `Activity`'s window holds the singleton, forming a cycle the GC cannot collect
- B. The static `companion object` → singleton → `Analytics` → `activity` field, which the GC roots never release across recreation ✓
- C. Rotation creates a new Activity that inherits the old one's context, doubling the leak
- D. `Analytics` implements `LifecycleObserver`, so the lifecycle registry pins the Activity
Correct answer: B. A static (companion) reference is a GC root; because it transitively holds the destroyed Activity via `Analytics.activity`, the Activity can never be collected.
You have `sharedFlow = MutableSharedFlow<Int>(replay = 0, extraBufferCapacity = 0)` with the default `SUSPEND` overflow strategy. A producer calls `sharedFlow.tryEmit(x)` while there are zero active collectors. What is the result?
- A. `x` is buffered and delivered to the next collector that subscribes
- B. `tryEmit` returns `false` and `x` is dropped because there is no buffer or replay to hold it ✓
- C. `tryEmit` suspends until a collector appears
- D. `tryEmit` throws an `IllegalStateException` due to buffer overflow
Correct answer: B. With replay 0 and no buffer, `tryEmit` cannot suspend and has nowhere to place the value with no collectors, so it returns `false` and the value is dropped.
An offline-first app uses Room as the single source of truth. Two devices edit the same record offline; both later sync. To resolve conflicts deterministically while preserving concurrent edits from being silently lost, which strategy best fits a senior design?
- A. Last-write-wins using each device's local wall-clock timestamp on the mutation
- B. Server-assigned version numbers with optimistic concurrency: reject stale writes and surface a merge, using per-field or vector clocks to detect true concurrency ✓
- C. Always prefer the device with the lower device ID to break ties
- D. Disable offline writes on all but one designated primary device
Correct answer: B. Wall clocks skew across devices; server versioning/vector clocks detect genuine concurrent edits and let you merge rather than blindly overwrite, which is the consistency-preserving senior choice.
`collectLatest { item -> processLongRunning(item) }` is applied to a flow emitting rapidly. `processLongRunning` does blocking I/O with no suspension points. What actually happens on fast emissions?
- A. Each new emission cancels and restarts `processLongRunning`, so only the last item's work completes
- B. New emissions cannot cancel the in-flight block because cancellation is cooperative and the blocking call never checks for it ✓
- C. The flow buffers all emissions and runs them sequentially to completion
- D. `collectLatest` throws because the collector cannot keep up with the producer
Correct answer: B. `collectLatest` cancels the previous block on a new emission, but cancellation is cooperative—code with no suspension points never observes the cancel, so it runs to completion regardless.
An app has a cold start of 1.8s. Systrace shows most time in class loading and JIT during the first frames of the main screen. Which remediation most directly targets this symptom?
- A. Move all initialization into a `ContentProvider` to run it earlier
- B. Ship a Baseline Profile so hot code paths are AOT-compiled at install time instead of JIT-compiled at runtime ✓
- C. Increase the heap size in the manifest to reduce GC pauses
- D. Wrap startup work in `WorkManager` with an expedited request
Correct answer: B. Baseline Profiles pre-compile (AOT) the critical startup/scroll code paths at install, eliminating interpretation/JIT overhead that dominates the first frames of cold start.
A LiveData observer keeps firing after the Fragment view is destroyed, causing crashes. The correct fix is:
- A. Observe using the Fragment instance as lifecycle owner
- B. Observe using viewLifecycleOwner in onViewCreated ✓
- C. Remove all observers in onDestroy manually always
- D. Switch LiveData to a plain callback
Correct answer: B. Using viewLifecycleOwner ties observation to the view lifecycle, avoiding stale observers after view destruction.
In Compose, wrapping expensive work in remember(key) recalculates only when:
- A. Every recomposition regardless of key
- B. The key changes between recompositions ✓
- C. The Activity restarts
- D. The app is backgrounded
Correct answer: B. remember(key) recomputes and caches its value only when the provided key changes across recompositions.
You call a suspend function from a ViewModel; when the user leaves the screen, the request should cancel automatically if launched in:
- A. GlobalScope
- B. viewModelScope ✓
- C. A raw newSingleThreadExecutor
- D. Dispatchers.IO directly without a scope
Correct answer: B. viewModelScope is cancelled when the ViewModel is cleared, automatically cancelling its coroutines.
Recomposition in Compose is skipped for a composable when its parameters are:
- A. Mutable and unstable
- B. Stable and unchanged (equals returns true) ✓
- C. Always nullable
- D. Wrapped in remember
Correct answer: B. Compose skips recomposition when a composable's inputs are stable and equal to the previous values.
An ANR is triggered when the main thread is blocked for roughly how long on input dispatch?
- A. About 500 milliseconds
- B. About 5 seconds ✓
- C. About 20 seconds
- D. About 60 seconds
Correct answer: B. Input-dispatch ANRs fire when the main thread fails to respond within about 5 seconds.
To ensure a cold Flow from Room stops collecting when the UI is not visible, you should collect it with:
- A. collect { } in onCreate
- B. repeatOnLifecycle(STARTED) / flowWithLifecycle ✓
- C. GlobalScope.launch
- D. LiveData.observeForever
Correct answer: B. repeatOnLifecycle(STARTED) suspends and cancels collection as the lifecycle drops below STARTED, saving resources.
Two coroutines update shared mutable state causing a race. The idiomatic Kotlin fix is:
- A. Add @Synchronized to a suspend function
- B. Confine updates via a Mutex or a single-threaded/actor context ✓
- C. Use volatile on the coroutine
- D. Switch to Dispatchers.Unconfined
Correct answer: B. A Mutex or single-threaded confinement serializes access to shared state, preventing the race safely with suspension.
With R8/ProGuard enabled, a reflection-based library crashes in release only. The correct remedy is:
- A. Disable minification for the whole app
- B. Add keep rules for the reflected classes/members ✓
- C. Rename the classes to shorter names
- D. Move code to a different package
Correct answer: B. R8 strips/renames unused or reflected symbols; -keep rules preserve classes accessed via reflection at runtime.
Passing a large Bitmap between Activities via Intent extras risks:
- A. A NullPointerException always
- B. TransactionTooLargeException due to Binder buffer limits ✓
- C. Slower Gradle sync
- D. A ClassNotFoundException
Correct answer: B. The Binder transaction buffer (~1MB) is limited; large payloads in extras throw TransactionTooLargeException.
In Compose, deriving a value from other state without extra recompositions is best done with:
- A. mutableStateOf on every read
- B. derivedStateOf ✓
- C. LaunchedEffect(Unit)
- D. remember without a key
Correct answer: B. derivedStateOf recomputes only when its underlying reads change, avoiding unnecessary recompositions of readers.
In Jetpack Compose, a lambda inside a frequently-recomposing composable is created anew each recomposition, breaking skipping of a child. Which approach preserves stability and avoids unnecessary recomposition?
- A. Wrapping the whole screen in a single composable
- B. Using remember (or referencing a stable method reference) so the lambda identity is preserved ✓
- C. Marking the child as @Composable twice
- D. Calling recompose() manually
Correct answer: B. Remembering the lambda (or using a stable reference) keeps a consistent instance so Compose can skip the unchanged child.
A background coroutine started in viewModelScope must NOT be cancelled when the user navigates away mid-write to the database. What is the correct pattern?
- A. Run it in GlobalScope so it never cancels
- B. Wrap the critical section in withContext(NonCancellable) ✓
- C. Use runBlocking on the main thread
- D. Catch CancellationException and swallow it
Correct answer: B. withContext(NonCancellable) protects a critical suspend section from cancellation while keeping normal scope semantics elsewhere.
Your app leaks memory because an inner AsyncTask/handler holds an implicit reference to the destroyed Activity. What is the root cause and correct fix?
- A. The Activity is static; make it non-static
- B. A non-static inner class holds an implicit outer Activity reference; use a static/nested class with a WeakReference or lifecycle-aware scope ✓
- C. Garbage collection is disabled; enable it
- D. The Activity uses too many views; reduce them
Correct answer: B. Non-static inner classes capture the enclosing Activity; using a static class with a WeakReference (or lifecycle-scoped work) avoids the leak.
In Compose, you observe janky scrolling in a LazyColumn showing thousands of items. Which combination most improves performance?
- A. Providing stable keys via the key parameter and hoisting heavy work out of the item composable ✓
- B. Wrapping each item in its own remember { } with random keys
- C. Replacing LazyColumn with a Column inside a verticalScroll
- D. Recomposing the entire list on every scroll frame
Correct answer: A. Stable item keys let Compose reuse/skip items correctly, and moving heavy computation out of item scope keeps frames fast; a scrollable Column would compose all items eagerly.
You need cold-start latency reduction for an app with heavy initialization. Which App Startup / initialization strategy is most correct?
- A. Do all initialization synchronously in Application.onCreate()
- B. Use lazy initialization and the App Startup library to defer/parallelize non-critical initializers off the critical path ✓
- C. Move everything to a ContentProvider so it runs earlier and blocks startup
- D. Initialize on the main thread inside the launcher Activity's onResume repeatedly
Correct answer: B. Deferring and parallelizing non-critical initializers (e.g., via App Startup and lazy init) shortens the critical startup path.
A Flow collected in the UI keeps running when the app goes to the background, wasting resources. Which operator/API safely stops collection when the lifecycle is stopped and restarts it later?
- A. flowOn(Dispatchers.Main)
- B. repeatOnLifecycle(Lifecycle.State.STARTED) around the collect ✓
- C. buffer() with unlimited capacity
- D. conflate() on the flow
Correct answer: B. repeatOnLifecycle(STARTED) cancels collection when the lifecycle drops below STARTED and re-launches it when it returns.
Two coroutines update shared mutable state concurrently, causing a race. Which is the most idiomatic Kotlin coroutine solution that avoids blocking?
- A. Use synchronized(this) blocks around each update
- B. Use a Mutex with withLock, or confine state to a single-threaded dispatcher/actor ✓
- C. Use Thread.sleep to space out updates
- D. Mark the variable @Volatile only
Correct answer: B. A coroutine Mutex.withLock (or confining mutations to a single dispatcher) provides non-blocking mutual exclusion; @Volatile alone doesn't make compound updates atomic.
In a ViewModel exposing UI state, you must ensure that a one-time event (e.g., a navigation or snackbar) is not re-emitted after a configuration change re-subscribes the collector. What is the correct approach?
- A. Expose it as StateFlow with the event as the value
- B. Model events with a Channel/SharedFlow (replay 0) or a consumed one-shot event wrapper ✓
- C. Store the event in a static variable
- D. Emit it from LiveData with postValue repeatedly
Correct answer: B. One-time events need a hot stream with no replay (Channel or SharedFlow replay=0) or a consumed-event wrapper so they fire once, not on re-subscription.
Your Room migration adds a NOT NULL column with no default to an existing table and the app crashes on upgrade for existing users. What is the correct migration handling?
- A. Set fallbackToDestructiveMigration for production without warning
- B. Provide an explicit Migration that ALTERs the table adding the column with a default or backfill for existing rows ✓
- C. Bump the version number only and let Room auto-handle it
- D. Delete the database file on every launch
Correct answer: B. Adding a NOT NULL column to existing rows requires an explicit migration supplying a default/backfill; destructive migration would wipe user data.
You must securely store an authentication token on-device. Which approach best resists extraction on a rooted device?
- A. Plain SharedPreferences
- B. Encrypting via a key stored in the Android Keystore (hardware-backed when available) ✓
- C. Hardcoding it in the source and obfuscating with R8
- D. Storing it Base64-encoded in a local file
Correct answer: B. The Android Keystore keeps key material in hardware-backed secure storage that is not directly extractable, unlike plaintext or reversible encodings.