Android · 14 min
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.
Android · 16 min read
Android technical interview questions and answers: lifecycle, process death, coroutines, data, security and testing, with the follow-up to each.
This page is for Android engineers at any level from mid to senior who have a technical round booked and want the questions with answers they can say out loud. You get fifteen Android technical interview questions, grouped by the part of the platform they test, a spoken answer for each, the trap, and the follow-up the interviewer is most likely to ask next. It ends with a worked example on process death and a side-by-side of the same question scored at mid level and at senior level.
The answers are longer than on the senior Android guide, because this page is built to be rehearsed, not skimmed.
The first question in a technical round usually tests recall. The second tests whether that recall is attached to a model. From the hiring side, the pushes come in four shapes, and every question below has at least one of them:
StateFlow holds state, and the next question is whether an equal value can ever arrive twice. The point is to see you narrow the claim yourself.When your first answer was incomplete, do not defend it. Tighten it: “Let me narrow that. It holds when this constraint applies, and it breaks in this failure case, so I would treat them separately.” Saying that costs you nothing. Refusing to correct an overclaim does.
Spoken answer: “Work lives with the narrowest lifecycle owner that needs it, and I collect with repeatOnLifecycle(Lifecycle.State.STARTED), or collectAsStateWithLifecycle in Compose. When the UI stops, collection is cancelled, not paused, and it restarts on return. Work that several screens share is produced once, upstream in the ViewModel, not relaunched by each fragment.”
The trap: launchWhenStarted, which suspends the collector but leaves the upstream running.
The follow-up: “You put two collect calls inside one repeatOnLifecycle block and the second never runs. Why?” collect on a flow that does not complete never returns, so the second line is never reached. Launch each collector as a sibling inside the block.
Spoken answer: “Since Android 14 a foreground service has to declare a type that matches what it does, and start within the platform’s timing rules with a notification the user can see. So I use one only for work the user is aware of, like an active upload or navigation. Deferrable work, like a nightly sync, is WorkManager with constraints. The platform has narrowed this every year, so I design for the current rules instead of retrofitting types release by release.”
The follow-up: “It is a nightly sync that can wait and never touches the user. Which service type makes it eligible?” None. It is scheduled work, and saying so is the answer.
Spoken answer: “No. It guarantees that eligible persistent work runs eventually, subject to its constraints, system quotas and scheduling. It is not an exact alarm. If the product needs an exact time, that is a different API with its own permission, and I would first ask whether the product really needs exact.”
Spoken answer: “No. It survives a configuration change. When the system kills the process in the background and the user returns, the activity and back stack are recreated, but the ViewModel is new and empty. Only what I saved comes back: small values in SavedStateHandle, and anything valuable in the database.”
The trap: this is one of the answers you must not get wrong. “The ViewModel keeps it” ends the question badly.
The follow-up: “How do you test that?” Background the app and kill the process with adb shell am kill <package>, or terminate it from Android Studio, then return from recents. Rotation alone does not test it.
Spoken answer: “They all write into the same saved-state bundle, so I choose by owner. rememberSaveable for small values a composable owns. SavedStateHandle for screen state the ViewModel owns and business logic reads. onSaveInstanceState for View-based screens or custom Views. None of them is durable storage.”
The trap: keeping the same value in both rememberSaveable and SavedStateHandle. They drift apart.
The follow-up: “The system kills the process and nothing about the draft was written to the database. Which of these keeps the large object alive?” None. Persist the draft and keep only its key in saved state.
The full set of Kotlin and coroutine questions, with cancellation, structured concurrency and Flow sharing, is on Kotlin interview questions. Two come up in almost every Android technical round, so they are here too.
Spoken answer: “Not by itself. suspend means the function can suspend, not that it runs on another thread. The author of the function has to make blocking work main-safe, usually with withContext(Dispatchers.IO) around the blocking call. Room and Retrofit suspend functions already do that, so wrapping them again adds nothing.”
The follow-up: “This coroutine runs on Main and calls a database API that blocks and never suspends. What in coroutines takes it off the main thread?” Nothing moves it automatically. That is the point of the question.
Spoken answer: “StateFlow for a current value every collector can read immediately. SharedFlow for broadcasts where I choose replay, buffering and overflow. Neither makes an event reliable on its own. If a snackbar is emitted while the collector is stopped, I have to decide whether losing it is acceptable.”
Compose has its own page: Jetpack Compose interview questions, with twelve questions on state, recomposition and side effects. The one sentence to have ready for a general technical round: remember survives recomposition, rememberSaveable survives recreation, and neither is a substitute for a real owner of the data.
Spoken answer: “Room runs migrations in sequence: if you ship 1-to-2 and 2-to-3, someone on version 1 goes through both. A crash for users two versions back means a gap in the chain. The rule is that every consecutive pair ships a migration, permanently. I export schemas, and a test with MigrationTestHelper creates old databases with real data, runs every path to the current version and checks the data survived.”
The trap: fallbackToDestructiveMigration as the safety net. It does not crash. It deletes the user’s data.
The follow-up: “The migration keeps every column, the schema validates, and the data is still wrong.” runMigrationsAndValidate checks the schema, not the data. So the test creates a database at each old version with MigrationTestHelper, inserts real rows, migrates to the current version, then reads the data back: row counts, default values, foreign keys and the invariants your app relies on. Run that for every version you still support in CI, and a missing migration step breaks the build rather than a user’s upgrade.
Spoken answer: “When the operation is idempotent and the failure is one a retry can fix. A timeout has three possible histories: the request never arrived, it arrived and failed, or it succeeded and the response was lost. The client cannot tell them apart. So GETs can retry with capped exponential backoff and jitter. A payment POST retries only with an idempotency key the server recognises, created once and persisted before the first attempt.”
The follow-up: “The payment timed out after the server committed it, and the user taps again.” Same operation id, so the server returns the first result instead of charging twice.
Spoken answer: “An OkHttp Authenticator handles the 401s, and the refresh is serialised per session, so twenty failures produce one refresh. Inside the lock, each request first checks whether the token it failed with is still current; if another request already refreshed, it retries with the new token. The refresh call goes through a client without the authenticator, so it cannot recurse, and a request gives up after one retry.”
Spoken answer: “Keys live in the Android Keystore, hardware-backed where the device supports it, and never leave it. Data that needs confidentiality is encrypted with those keys, for example with Tink over DataStore. Nothing sensitive goes in plain preferences, logs or backups. And local encryption is one layer: tokens are short-lived and revocable, and the server authorises every protected action.”
The trap: recommending EncryptedSharedPreferences without saying that the Jetpack Security crypto library is deprecated. The answer used to be right. That is what makes it a tell.
Spoken answer: “I pin public keys, not certificates, so a renewed certificate with the same key still matches. There are always two pins or more: today’s key plus a spare key made ahead of time and stored offline, and I use Network Security Config with an expiration date so a forgotten pin cannot lock out old installs forever. Pinning adds to normal TLS validation. It never replaces it.”
The follow-up: “The certificate renews with a new key while last year’s app version is still installed. What keeps those users online?” The backup pin that was already in that version. That is why it has to ship before it is needed.
Spoken answer: “runTest with virtual time, test dispatchers injected and sharing one scheduler, a rule for Dispatchers.Main, and Turbine or explicit collection to assert the sequence of states. No Thread.sleep, no real delays. I assert on the states the user would see, not on operator internals, so a harmless refactor does not break the test.”
The follow-up: “It passes locally and hangs in CI.” Usually a dispatcher that was not injected and runs on a different scheduler. Inject every one.
Spoken answer: “An integration test wired with the real modules, calling across the boundary the way production does. Both modules’ mocks can encode the same wrong assumption. It runs after the fast module tests and before the slow end-to-end suite, and it is required in CI.”
Spoken answer: “The main thread not processing input in time: disk or database work on main, lock contention with a background thread, slow broadcast receivers, binder calls to a busy system service. I read the trace before guessing, because ‘slow code’ is often a lock. Prevention is I/O off main, StrictMode in debug builds, and production monitoring through Play Vitals and ApplicationExitInfo.”
The follow-up: “An I/O coroutine holds a mutex through a network call, and the main thread blocks waiting for it. Which dispatcher fixes that?” None. The design has to stop the main thread depending on a lock held across blocking work.
Prompt: “A user is on step three of a checkout form, backgrounds the app to find a card, and the system kills the process. What do they see when they come back?”
Mid-level answer: “The ViewModel keeps the form state, so they continue where they were.”
Senior answer: “They see step one with empty fields, unless we designed for it. I separate four kinds of state. Transient UI state, like whether a hint is expanded, can be lost. The current step and the draft’s id go in SavedStateHandle: small, and restored after process death. The draft itself goes in Room, saved as the user types, because it is valuable and may be too large for the bundle. And server data, like available delivery slots, is reloaded fresh, because the saved copy may be stale.”
class CheckoutViewModel(
private val handle: SavedStateHandle,
private val drafts: DraftRepository,
) : ViewModel() {
// Small and restorable: survives process death through the saved-state bundle.
val step: StateFlow<Int> = handle.getStateFlow("step", 0)
private val draftId: String =
handle.get<String>("draftId") ?: UUID.randomUUID().toString().also { handle["draftId"] = it }
// Valuable: lives in the database, the bundle only holds its key.
val draft: StateFlow<Draft?> = drafts.observe(draftId)
.stateIn(viewModelScope, SharingStarted.WhileSubscribed(5_000), null)
fun next() { handle["step"] = step.value + 1 }
fun update(change: Draft) { viewModelScope.launch { drafts.save(draftId, change) } }
}The push: “The form has a photo attachment, and saved state is now too large.” A large bundle can crash with TransactionTooLargeException. Store the file in app storage, keep its reference in the draft row, and keep only ids in saved state.
The push after that: “The delivery slot they picked no longer exists.” Restore the draft, then reconcile it against fresh data and tell the user what changed. That last sentence is the one that separates the senior answer: restoration is not replaying old state, it is rebuilding a correct screen.
The question: “The ViewModel is cleared, yet a coroutine you started in viewModelScope keeps going. Why?”
| Mid-level answer | Senior answer | |
|---|---|---|
| First sentence | “Use viewModelScope so it gets cancelled.” | “A child of viewModelScope is cancelled when the ViewModel is cleared, so something detached it or ignores cancellation.” |
| Where it looks | The launch call | The job’s parent, blocking calls that never check cancellation, a swallowed CancellationException |
| The fix | Cancel it in onCleared() |
Remove the detached launch, make the blocking boundary cancellable, move work that must outlive the screen to an owner like WorkManager |
| Passes at | Mid level | Senior level |
The mid-level answer is not wrong. It describes the contract. The senior answer names the ways code escapes the contract, which is what the question was about. The same pattern holds for most questions on this page: at mid level, knowing the API passes; at senior level, the interviewer expects the failure mode without having to prompt for it.
Cover each spoken answer and say your own version out loud in under thirty seconds. Then answer the follow-up without looking. Mark every question where the second answer is weaker than the first; those are the ones to rehearse. If you are choosing a book for the rest of the loop, Android interview books compares them by how much time you have left. For the design rounds, go on to Android architecture interview questions and Android system design interview.

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

Android · 13 min
Kotlin interview questions as Android loops ask them: null safety, coroutines, cancellation, Flow, sealed types and generics, with each follow-up.

Android · 12 min
What an Android coding interview asks: the practical tasks to prepare, the algorithm round in Kotlin, and what is graded beyond a screen that works.