These are the questions Android interviews actually ask, answered briefly and against the app this track built. A good interview answer is short, says why, and can point at real code — so each answer here names the lesson where the full reasoning lives.
Lifecycle and state
What happens to an Activity when the phone rotates?
It is destroyed and recreated with the new configuration. Everything held by the Activity or in plain
remember is lost. A ViewModel survives, because it lives in a store that
outlives the Activity instance; rememberSaveable and SavedStateHandle values
survive because they are written to the saved-instance Bundle. (Lesson 5.)
What is process death, and how do you handle it?
When the app is in the background, Android may kill its process to reclaim memory, then restore the
task — same screen, same back stack, same saved state — in a new process when the user returns. Every
ViewModel and singleton is new and empty. You handle it by saving what the user typed or chose in
SavedStateHandle or rememberSaveable, saving ids rather than objects, and
re-fetching data. You test it with adb shell am kill on a backgrounded app. The app's checkout
keeps its form in the handle for exactly this reason. (Lesson 5.)
What does a ViewModel survive, and what must it never hold?
It survives configuration changes and lives as long as its owner — a navigation destination or an
Activity — is alive; its scope is cancelled when it is cleared. It must never hold an Activity, a View or
an Activity Context, because it outlives them and would leak a destroyed Activity. That rule
is why the app's ViewModel cannot own Stripe's payment sheet. (Lessons 5 and 18.)
remember versus rememberSaveable versus a ViewModel?
remember survives recomposition only. rememberSaveable also survives rotation
and process death but must fit in a Bundle. A ViewModel survives rotation, holds work that
must not restart, and is reachable from business logic. Choose by who owns the value and how long it must
live. (Lesson 4.)
Compose
What is recomposition, and what triggers it?
Re-running a composable function because observable state it read has changed. Only Compose
State — or a flow collected into one — triggers it; mutating a plain variable or mutating a
collection in place does not. Compose skips any composable whose inputs are unchanged. (Lesson 4.)
Why would a list item not update when you add to a list in state?
Because the list was mutated in place, so the state box still holds the same instance and nothing was notified. Hold immutable values and replace them; the app's builder rebuilds its topping set on every toggle for this reason:
fun toggle(topping: Topping) {
selectedToppingIds = if (topping.id in selectedToppingIds) {
selectedToppingIds - topping.id
} else {
selectedToppingIds + topping.id
}
}
What are stability and strong skipping?
Compose skips a composable whose parameters have not changed. Historically it could only trust
stable types, so a List parameter meant the composable never skipped. Since Kotlin
2.0.20, strong skipping is the default: unstable parameters are compared by instance and lambdas are
remembered automatically. @Immutable and @Stable are still promises you can make,
and a broken promise causes missed updates. (Lesson 7.)
What is state hoisting?
Moving state up to the lowest common owner and passing it down as a value plus a callback, so the
component is stateless. The app's quantity stepper takes value and
onValueChange, which is what lets it drive both a local builder quantity and a cart line in a
store. (Lesson 4.)
Why does a LazyColumn need keys?
Without keys, items are identified by position, so state follows the slot rather than the item when the
list changes. Keys keep each item's state with the item, enable animateItem, and anchor the
scroll position — which can hide an item inserted above the first visible one. (Lesson 7.)
How do you trigger a one-off action, like navigation, from a ViewModel?
Not from the composable's body, which may run many times. The app models the event as state — a
nullable request — which the screen handles in a LaunchedEffect keyed on it, then reports
back so the ViewModel clears it:
fun onPaymentSheetPresented() {
_state.update { it.copy(paymentRequest = null) }
}
Clearing it at handling time means a rotation cannot repeat it. (Lessons 6 and 18.)
Coroutines and Flow
What is structured concurrency?
Every coroutine runs in a scope that owns it: cancelling the scope cancels its children, and a child's
failure reaches its parent. Nothing is left running that nobody can stop. The app uses
viewModelScope for screen work, an injected application scope for work that must outlive a
screen, and coroutineScope { } for parallel requests that should fail together. (Lesson
13.)
Why must you rethrow CancellationException?
Cancellation is delivered as that exception. Swallowing it in a broad catch keeps a
cancelled coroutine running and reports the cancellation as an error to a user who simply left the
screen. Every catch (e: Exception) in the app is preceded by one that rethrows cancellation.
(Lesson 12.)
StateFlow or LiveData?
StateFlow in new code. It is Kotlin rather than Android, so it works in pure domain code
and tests without Android; it has operators (map, stateIn,
distinctUntilChanged); and Compose collects it lifecycle-aware with
collectAsStateWithLifecycle. LiveData remains in older codebases and is fine to read.
How would you debounce saves?
Cancel the pending job and launch a new one that delays first. Cancellation is the debounce:
persistJob?.cancel()
persistJob = scope.launch {
delay(persistDebounce)
persist()
}
And on a phone, flush immediately when the app leaves the screen, because the process may be killed before the delay ends. (Lessons 13 and 17.)
Mutex or synchronized?
A Mutex in coroutine code. synchronized blocks the waiting thread — possibly
the main thread — while withLock suspends the coroutine and frees the thread. (Lesson 13.)
Architecture
Describe your app's architecture.
Layers with dependencies pointing inwards: features depend on the domain; the domain depends on
nothing; the data layer implements repository interfaces the domain declares. Rules live in pure functions
— the cart reducer — and effects in stores. State flows down as StateFlow, events flow up as
callbacks. (Lesson 14.)
Why put the cart's rules in a reducer?
Because a reducer is a pure function from state and action to state: testable with no app, every mutation named, and a sealed action type the compiler checks for exhaustiveness. It earns its place where state is shared and the rules are real; for local form state it would be ceremony.
is CartAction.SetOrderType -> state.copy(orderType = action.orderType)
is CartAction.Hydrate -> action.state
// The order type survives: a customer who chose pickup and then ordered has not asked to
// switch back to delivery.
CartAction.Clear -> state.copy(items = emptyList())
How do you model loading, success, empty and error?
As one sealed type, not three booleans, so impossible combinations cannot be represented and a screen's
exhaustive when cannot forget the failure branch:
sealed interface UiState<out T> {
data object Idle : UiState<Nothing>
data object Loading : UiState<Nothing>
data class Loaded<T>(val value: T) : UiState<T>
data object Empty : UiState<Nothing>
data class Failed(val message: String, val isRetryable: Boolean) : UiState<Nothing>
// …
Dependency injection
What does Hilt give you over manual DI?
A generated, compile-time-checked graph: you declare what each class needs and how to build what Hilt cannot work out, and it builds everything in the right order, scoped to Android lifetimes, failing the build on a missing binding. Manual DI — the iOS version of this app's composition root — is perfectly viable for small apps and needs no framework. (Lesson 15.)
@Provides or @Binds?
@Binds to map an interface to an implementation that already has an @Inject
constructor; @Provides to build something Hilt cannot construct itself — a third-party type,
or a class that needs a value Hilt cannot choose, like the cart's debounce.
How do you swap dependencies in a test?
For unit tests, pass fakes to constructors — no Hilt involved. For a test of the real graph,
@TestInstallIn(replaces = …) swaps one module for a fake one. (Lessons 15 and 20.)
Platform
compileSdk, minSdk, targetSdk?
compileSdk is the API you compile against — what you may call; it changes nothing at
runtime. minSdk is the oldest version the app installs on. targetSdk is the
version whose behaviour you have tested against; raising it opts out of compatibility shims, and Play
requires it to stay recent. (Lesson 1.)
Which Context should you use?
The application Context for anything that outlives a screen — DataStore, the Keystore,
Stripe's API client — because it lives as long as the process. An Activity Context only for
things tied to that Activity, such as showing UI, and never stored in a ViewModel or singleton. Hilt's
@ApplicationContext qualifier makes the safe choice explicit.
What is an ANR?
"Application Not Responding": the main thread was blocked for about five seconds, so the system offers
to close the app. The cause is blocking work on the main thread — disk, network, a lock. Main-safe suspend
functions, DataStore instead of SharedPreferences, and Mutex instead of
synchronized are all ways this track avoids it. (Lessons 13 and 16.)
Where would you store an auth token?
Encrypted at rest with a key in the Android Keystore, which never leaves the device, and excluded from backup. Not in plain SharedPreferences or DataStore, and not via the deprecated EncryptedSharedPreferences. (Lesson 16.)
Why does a release build crash when debug works?
Most often R8: something reached only through reflection was removed or renamed. Libraries ship consumer keep rules; avoid reflection where you can (kotlinx.serialization generates serialisers at compile time); and run a minified build against a real backend before shipping. (Lesson 22.)
How would you make a screen testable?
Split it into a route that talks to Hilt and a stateless content composable that takes a
UiState and lambdas; keep rules in plain classes; depend on interfaces at the network
boundary; inject scopes and dispatchers. Then the rules test on the JVM, the network against
MockWebServer, the ViewModel with a test dispatcher, and the UI through the semantics tree. (Lesson
20.)
How to use these
Do not memorise the answers. Every one of them is a claim about code, and the strongest interview answer is "here is a case where it mattered" — the order that would not appear in a list, the rotation that would have opened a second payment sheet, the lint check that failed one build in two. Build something, break it on purpose, and you will have those stories of your own.