Android Interview Questions: Live Practice Quiz
Reviewed by Mark Dickie · Last updated
Android is Google's mobile operating system, built on the Linux kernel and running on phones, tablets, watches, TVs, and cars. For an Android interview, you should know the activity and fragment lifecycle inside out, how Jetpack libraries (ViewModel, Room, Navigation, WorkManager) fit together, and how coroutines handle async work on the main thread. Expect questions on RecyclerView performance, dependency injection with Hilt, testing strategies, and the architecture pattern the team uses, usually MVVM or Clean Architecture. Memory leaks and ANRs are performance topics that often separate mid-level from senior candidates.
What does an Android interview typically test?
| Area | What comes up | Difficulty range |
|---|---|---|
| Activity & Fragment lifecycle | State transitions, configuration changes, saved state | 1–4 |
| Jetpack components | ViewModel, LiveData, Room, Navigation, WorkManager | 2–4 |
| Coroutines & concurrency | suspend functions, Flow, structured concurrency, dispatchers | 3–5 |
| RecyclerView | ViewHolder pattern, DiffUtil, adapter optimization | 2–4 |
| Dependency injection | Hilt vs Dagger, scoping, component hierarchy | 2–4 |
| Testing | JUnit, Espresso, Compose testing, mocking | 2–5 |
| Architecture | MVVM, Clean Architecture, separation of concerns | 2–5 |
| Performance | Memory leaks, ANRs, startup time, battery | 3–5 |
How should you prepare for an Android interview?
- Review the activity lifecycle callbacks and what triggers each one. Configuration changes, process death, and back navigation all behave differently.
- Build a small app using Room, ViewModel, and coroutines together so you can explain how data flows from database to UI on the main thread safely.
- Practice writing unit tests with MockK or Mockito, and at least one instrumentation test with Espresso.
- Study common memory leak causes: static references to Activities, unregistered listeners, and observer leaks in LiveData.
- Read through the Jetpack library docs for the libraries the job description mentions. Most teams use a specific subset, and knowing those in depth beats a shallow survey of all of them.
Key facts
- Tarmac has 96 Android interview questions on this topic, 25 of them on this page, at difficulty 1–5 of 5.
- Tarmac last reviewed these Android interview questions on 31 August 2026.
At a glance
| Questions | 25 shown · 96 in the bank |
|---|---|
| Difficulty | 1–5 of 5 |
| Formats | Code output, Flashcard, Multiple choice, True / false, Ordering, Fill in the blank, Multiple answer, Short answer, Find the bug |
What you'll review
- dependency management
- workmanager
- configuration changes
- runtime permissions
- anr main thread
- intent filters
- activity lifecycle
- gradle build variants
- fragment lifecycle
- room database
- explicit implicit intents
- services vs workers
- memory leaks lifecycle
- dao entities migrations
Practice questions
Android/build-system/dependency-management
Given the following Gradle build snippet, what is printed to the console when this configuration is evaluated?#
ext {
coroutinesVersion = "1.7.3"
}
dependencies {
implementation "org.jetbrains.kotlinx:kotlinx-coroutines-android:${coroutinesVersion}"
}
println "Resolved dependency: org.jetbrains.kotlinx:kotlinx-coroutines-android:${coroutinesVersion}"Show answer
Resolved dependency: org.jetbrains.kotlinx:kotlinx-coroutines-android:1.7.3
In the Groovy DSL, ext defines a project-level extra property. coroutinesVersion is set to the string 1.7.3. The println uses Groovy string interpolation (${coroutinesVersion}), which substitutes the property value into the string, producing Resolved dependency: org.jetbrains.kotlinx:kotlinx-coroutines-android:1.7.3. The dependencies block does not affect the output of the println statement.
Android/background-work/workmanager
What is WorkManager, and when should you use it for background work?#
Show answer
Use WorkManager for deferrable, guaranteed execution of background tasks — tasks that need to run even if the app exits or the device restarts. For immediate, non-persistent background work, prefer Kotlin Coroutines or other in-process alternatives.
WorkManager is the recommended Android Jetpack library for persistent, deferrable background work. It guarantees execution across app death and device reboots, which distinguishes it from in-memory coroutine-based work that is lost when the process dies.
Android/component-lifecycle/configuration-changes
An Activity declares no android:configChanges in the manifest. What happens to that Activity by default when the user rotates the device?#
Options
Show answer
An Activity that declares no android:configChanges is destroyed and recreated by default when the device rotates. The system calls onSaveInstanceState() before tearing down the old instance so small amounts of UI state, such as scroll position or text field contents, can be written into a Bundle that the new instance reads back in onCreate() or onRestoreInstanceState(). Declaring android:configChanges for a specific configuration type is what opts an Activity out of this default and routes the change to onConfigurationChanged() instead.
A configuration change like a rotation, by default, is treated as if the current Activity can no longer handle its current configuration: the system tears it down and creates a fresh instance against the new configuration (new resources, layout, etc.). Before that teardown, onSaveInstanceState(Bundle) is called so small amounts of UI state (scroll position, text field contents) can be written into a Bundle that the new instance receives in onCreate()/onRestoreInstanceState(). Only declaring android:configChanges for the specific configuration types (e.g. orientation|screenSize) opts an Activity out of this and routes the change to onConfigurationChanged() instead — the exception, not the default. This is precisely why non-trivial state belongs in a ViewModel (which survives the recreation) rather than in Activity fields, which is a very common interview probe.
Android/permissions/runtime-permissions
An app targeting a current Android version needs to read the user's precise location, a dangerous-protection-level permission declared in the manifest. What must the app additionally do before that code path can succeed on Android 6.0 (API 23) and above?#
Options
Show answer
For a dangerous-protection-level permission like precise location, the app must explicitly request it at runtime on Android 6.0 (API 23) and above, in addition to declaring it in the manifest. Declaring the permission only tells the system the app may ask for it; the app still has to prompt the user through the runtime permission flow, such as the Activity Result API's RequestPermission contract, and handle both the grant and the deny outcome, since normal-level permissions like INTERNET are the only kind granted automatically at install time.
Since Android 6.0, permissions in the "dangerous" protection level (location, camera, contacts, and similar) are runtime permissions: listing them in the manifest is necessary but not sufficient — it only tells the system the app may ask for them. The app must separately prompt the user at the point of use (or ahead of it, with a rationale via shouldShowRequestPermissionRationale()) and branch on whether the grant actually happened, because the user can deny it, or later revoke it in Settings even after granting it once. This is different from "normal"-level permissions (like INTERNET), which are granted automatically at install time with no runtime prompt at all — a distinction interviewers commonly probe because conflating the two is a frequent source of crashes (SecurityException) in production. <queries> is an unrelated manifest element for declaring package visibility, not a way to request a dangerous permission.
Android/performance/anr-main-thread
A long-running computation on a background thread, by itself, can trigger an Application Not Responding (ANR) dialog.#
Options
Show answer
False. A long-running computation on a background thread does not by itself trigger an ANR, because an ANR watchdog specifically monitors the main thread's ability to respond to input within roughly a 5-second budget, or a component like a BroadcastReceiver finishing within its own timeout. Moving expensive work off the main thread is exactly what prevents an ANR; the only way background work contributes to one is indirectly, by holding a lock the main thread ends up blocked waiting on.
False. An ANR is specifically about the main (UI) thread failing to respond — the system watchdog fires when the main thread can't process an input event (roughly a 5-second budget) or a component like a BroadcastReceiver doesn't finish within its own timeout. Work correctly moved off the main thread onto a background thread, by definition, cannot block the main thread's ability to process input, so it cannot by itself trigger an ANR — that's precisely the point of moving expensive work off the main thread in the first place. What background work can still cause is a different failure: if it eventually blocks on a lock the main thread is also waiting on, that indirect contention can produce an ANR, but that's a distinct mechanism from "a background thread being slow" on its own.
Android/intents-navigation/intent-filters
What is an implicit intent, and how does the system decide what handles it?#
Show answer
An implicit intent declares an action to be performed (and optionally a category and data URI/MIME type) without naming a specific target component — for example ACTION_VIEW with a URL. The system resolves it at dispatch time by matching it against every installed app's <intent-filter> declarations in their manifests, collecting every component whose filter matches the action, category, and data type. If exactly one component matches, it launches directly; if several match, the system shows the chooser (or disambiguation) UI so the user picks; if none match, starting it throws ActivityNotFoundException.
Implicit intents are what let apps interoperate without knowing each other's package names in advance — a share sheet, a "open this URL," a "pick a photo" all work because the requesting app only describes what it wants done, and the intent-filter mechanism is the system-wide registry that lets any installed app opt into handling that kind of request. Understanding this resolution flow is what lets an engineer correctly reason about deep links, share targets, and why resolveActivity() (or checking for null before startActivity) is defensive best practice — the set of apps able to handle a given implicit intent varies by device and by what else the user has installed.
Android/component-lifecycle/activity-lifecycle
Order the callbacks Android invokes as an Activity goes from first being launched to being fully destroyed when the user presses Back (a normal finish, not a configuration change).#
Put these in order
Show answer
As an Activity moves from launch to a normal finish via the Back button, Android calls onCreate(), then onStart(), then onResume(), then onPause() as the Back press begins the transition, then onStop() once it is fully off screen, and finally onDestroy() as the Activity instance is torn down. Each step hands off to the next in one direction: an Activity cannot become visible before it is created, cannot gain focus before it is visible, and loses focus before it loses visibility on the way out.
The lifecycle is a strict progression outward and then back in reverse: an Activity can't become visible before it's created, can't gain focus before it's visible, and on the way out it loses focus before it loses visibility, and loses visibility before it's torn down — each step hands off to the next in exactly one direction for a simple finish. onPause() fires as soon as the Back press starts the transition (the Activity is still technically on screen mid-transition), onStop() fires once it's fully off screen, and onDestroy() is the final cleanup once the system has decided the Activity instance itself is being discarded (as opposed to just being stopped and possibly resumed again if the user hadn't pressed Back). Getting this order backwards — e.g. assuming onStop() happens before onPause() — is a common early mistake that leads to releasing resources at the wrong time.
Android/background-work/workmanager
What guarantees does WorkManager provide for surviving process death and device reboot, and how does it achieve them?#
Show answer
WorkManager persists work requests in its own SQLite database, so enqueued tasks survive app restarts and device reboots. On reboot, WorkManager uses a BootReceiver to reschedule any still-pending or repeating work. Completed (SUCCEEDED) one-time work is NOT re-run. Jobs currently executing when the app is killed are rescheduled once the system wakes WorkManager again.
WorkManager stores every WorkRequest in an internal database. When the app process is killed or the device reboots, WorkManager's BootReceiver fires on BOOT_COMPLETED and reschedules all non-terminal work. One-time work that already reached SUCCEEDED is terminal and will not re-run; only work that is ENQUEUED, RUNNING (at time of death), or periodic gets rescheduled.
Android/build-system/dependency-management
The Kotlin snippet below simulates Gradle dependency version resolution: a version catalog provides default versions, a forceVersion map simulates resolutionStrategy.force, and any dependency absent from both falls back to "unspecified". What is printed to the console?#
// Simulating a Gradle version catalog with a force/override resolutionStrategy
val catalog = mapOf("okhttp" to "4.12.0", "retrofit" to "2.9.0", "moshi" to "1.14.0")
fun resolve(dep: String, forceVersion: Map<String, String>): String {
val v = forceVersion[dep] ?: catalog[dep] ?: "unspecified"
return "$dep:$v"
}
val forced = mapOf("retrofit" to "2.11.0")
listOf("okhttp", "retrofit", "moshi", "coil").forEach {
println(resolve(it, forced))
}Show answer
okhttp:4.12.0
retrofit:2.11.0
moshi:1.14.0
coil:unspecified
For each dependency, resolve checks the forceVersion map first, then the catalog, then defaults to "unspecified". okhttp has no forced override so it resolves to the catalog version 4.12.0. retrofit is forced to 2.11.0, overriding the catalog's 2.9.0. moshi resolves to 1.14.0 from the catalog. coil is in neither map, so it becomes coil:unspecified.
Android/build-system/gradle-build-variants
Given the Gradle configuration below with two flavor dimensions, fill in each blank with the correct dimension name so that every flavor is properly assigned.#
Show answer
Given the Gradle configuration below with two flavor dimensions, fill in each blank with the correct dimension name so that every flavor is properly assigned.
android {
flavorDimensions = ["mode", "api"]
productFlavors {
free {
dimension = **mode**
}
paid {
dimension = **mode**
}
minApi21 {
dimension = **api**
}
minApi24 {
dimension = **api**
}
}
}
What are the values of mode and api respectively?
The flavorDimensions list declares two dimensions: "mode" and "api". Each product flavor must be assigned to exactly one declared dimension via the dimension property. The flavors free and paid belong to the "mode" dimension, while minApi21 and minApi24 belong to the "api" dimension. Assigning a flavor to a dimension that is not declared in flavorDimensions would cause a Gradle build error. With this setup, the plugin generates variants such as freeMinApi21Debug, freeMinApi21Release, paidMinApi24Debug, etc.
Android/component-lifecycle/fragment-lifecycle
In the Android Fragment lifecycle, the callback responsible for inflating the fragment's layout XML and returning the root View is _____.#
Show answer
In the Android Fragment lifecycle, the callback responsible for inflating the fragment's layout XML and returning the root View is **onCreateView**.
onCreateView is the Fragment lifecycle method the system calls to ask the fragment to inflate and return its UI view hierarchy. The returned View is then passed to onViewCreated, making onCreateView the correct callback for layout inflation. onCreate is used for non-UI initialization, onViewCreated for view-reference setup, and onAttach runs before any view work happens.
Android/background-work/workmanager
A team needs to upload a batch of analytics events roughly every 6 hours, guaranteed to eventually run even if the app was closed or the device rebooted in between, and only while the device has network connectivity. Which approach fits that requirement best?#
Options
Show answer
For deferrable background work that must eventually run even across process death or a device reboot, WorkManager's PeriodicWorkRequest with a network Constraint is the correct fit. WorkManager persists the pending work and its constraints to its own on-disk database, so the schedule survives the app being closed or the device rebooting, and it dispatches to JobScheduler or AlarmManager under the hood depending on API level. A coroutine loop tied to the app process, an always-on foreground Service, or firing network calls directly from a BroadcastReceiver's main thread are each the wrong tool for this requirement.
WorkManager is purpose-built for exactly this shape of requirement — deferrable, guaranteed background work with constraints — which is why it's Google's recommended API for it. A PeriodicWorkRequest persists its schedule and constraints to WorkManager's own database, so the work survives process death and device reboots and resumes once its network constraint is satisfied again, without the app needing to still be running. A coroutine loop in Application.onCreate() dies the moment the process is killed by the system, which is routine on Android; a permanently-running foreground Service burns battery and is the wrong tool for infrequent, deferrable work; and firing network I/O directly inside a BroadcastReceiver on the main thread will both block the UI thread and likely exceed the receiver's short execution time limit, causing an ANR-adjacent crash. Note WorkManager's periodic work also has a platform-enforced 15-minute minimum interval, which a 6-hour cadence comfortably clears.
Android/data-persistence/room-database
Which of these are genuine, accurate behaviors of the Room persistence library? Select all that apply.#
Options
Pick every one that applies.
Show answer
Room genuinely verifies @Query SQL against the schema at compile time so a bad query fails the build rather than failing at runtime, and a suspend @Dao method is dispatched off the main thread automatically without the caller wrapping it in withContext. Bumping the @Database version with no matching Migration and no fallbackToDestructiveMigration() call makes Room throw at runtime rather than silently continuing. Room has no built-in Firestore conversion, and a synchronous query on the main thread throws IllegalStateException by default rather than being silently allowed.
Room's whole pitch is catching persistence bugs at compile time and keeping the database off the main thread by default: its annotation processor validates @Query strings against the real schema so a bad query fails the build (a), and a suspend DAO method is automatically run on a background dispatcher Room manages internally, no manual withContext needed (b). A missing Migration for a bumped version is treated as a real error — Room throws rather than silently trusting stale schema assumptions, specifically to prevent the far worse failure mode of reading/writing data with a schema that no longer matches (c). Room has nothing to do with Firestore or any cloud sync layer (d is invented) — it's purely a local SQLite abstraction — and by default Room actively throws IllegalStateException ("Cannot access database on the main thread") for a synchronous query, unless the dangerous allowMainThreadQueries() escape hatch is explicitly opted into, so (e)'s "allowed by default with no warning" is false.
Android/intents-navigation/explicit-implicit-intents
Which of these statements about Intents and PendingIntents are accurate? Select all that apply.#
Options
Pick every one that applies.
Show answer
An explicit intent names a target component directly, while an implicit intent declares an action that the system resolves against intent filters declared in the manifest, matched on action, category, and data. A PendingIntent wraps an Intent plus the permission to execute it later with the originating app's identity, which is why notifications and widgets use it instead of a raw Intent. On apps targeting Android 12 and above, constructing a PendingIntent requires an explicit FLAG_MUTABLE or FLAG_IMMUTABLE rather than defaulting silently to mutable, and starting an Activity with an implicit intent that resolves to nothing throws ActivityNotFoundException rather than no-op'ing.
The explicit-vs-implicit distinction is exactly (a): an explicit intent targets a specific component by class name, bypassing resolution entirely, while an implicit intent is resolved by the system against every app's declared intent filters (b describes why PendingIntent exists — a notification is drawn and shown by the system server, not the originating app, so it needs a token it can later fire with the originating app's identity and permissions, which is what a PendingIntent is). Intent filters in the manifest are precisely the mechanism that makes implicit-intent resolution possible, matched against action/category/data (d). On the other two: since Android 12, apps targeting API 31+ must explicitly specify PendingIntent.FLAG_MUTABLE or FLAG_IMMUTABLE when constructing one, and FLAG_IMMUTABLE is the one the platform pushes as the safer default — mutability is not silently allowed with no flag, so (c) is false. And calling startActivity() with an implicit intent that resolves to nothing throws ActivityNotFoundException rather than silently no-op'ing, which is exactly why the docs recommend calling resolveActivity() first to check — so (e) is false.
Android/component-lifecycle/fragment-lifecycle
A Fragment's own Lifecycle (from getLifecycle()) and its view's lifecycle (from getViewLifecycleOwner()) can be in different states, because the Fragment instance can outlive its View — for example while it's on the back stack and its view has been destroyed but the Fragment itself hasn't.#
Options
Show answer
True. A Fragment's own Lifecycle and its view's lifecycle from getViewLifecycleOwner() can be in different states because the Fragment instance can outlive its View — this happens routinely when a Fragment sits on the back stack, since its view is destroyed in onDestroyView() while the Fragment object itself stays alive until onDestroy(). Observing LiveData or Flow with the Fragment's own lifecycle instead of viewLifecycleOwner is a common source of crashes and duplicate-observer bugs for exactly this reason.
True. A Fragment's view is destroyed in onDestroyView() well before the Fragment instance itself is destroyed in onDestroy() — this happens routinely when a Fragment is pushed onto the back stack (its view is torn down to free memory) while the Fragment object stays alive, ready to have its view re-created in onCreateView() if the user navigates back. That's exactly why observing LiveData/Flow with the Fragment's own lifecycle (this / viewLifecycleOwner = fragment) instead of viewLifecycleOwner is a well-known source of bugs: the observer keeps running (and can crash trying to touch a destroyed view, or silently accumulate duplicate observers across view recreations) because it's scoped to the longer-lived Fragment lifecycle rather than the shorter-lived view lifecycle the UI code actually depends on.
Android/component-lifecycle/configuration-changes
Explain why a plain ViewModel survives a configuration change like device rotation but does not survive the process being killed by the system, and how SavedStateHandle closes that gap.#
Show answer
A ViewModel is retained across a configuration change because the framework deliberately keeps the same ViewModelStore alive across the Activity's destroy-and-recreate cycle that a rotation triggers — the old Activity instance is thrown away but its ViewModelStore, and therefore its ViewModels with all their in-memory fields, is handed to the new instance unchanged, so nothing needs to be serialized. That mechanism only works because the process itself keeps running through a configuration change. When the system kills the whole process to reclaim memory (the app is backgrounded and needs to be evicted), there is no in-memory ViewModelStore left to hand off at all — the process restarts from scratch and a brand-new ViewModel is created with none of its previous field values, exactly like any other in-memory object. SavedStateHandle closes that gap by writing a small amount of state into the same Bundle mechanism the system already uses for onSaveInstanceState, which the OS persists across process death (not just configuration changes) and hands back to the newly-created ViewModel on the next launch — so any state stored in it survives even the harder failure mode a plain ViewModel field cannot.
The distinction hinges on what actually gets destroyed: a configuration change destroys and recreates the Activity, but the process — and therefore anything held purely in memory, like a running ViewModel — keeps living, so the framework simply reattaches the existing ViewModelStore to the new Activity instance with zero serialization involved. Process death is a strictly harder failure mode: the whole process, including every in-memory object, is gone, so nothing can be 'handed off' at all — the only thing that survives is whatever was written to durable storage before death, which for Android is the Bundle written in onSaveInstanceState(). SavedStateHandle is the ViewModel-facing API onto that same Bundle mechanism, which is why it, and not a plain ViewModel field, is the one that also survives process death, not just rotation.
Android/build-system/dependency-management
The following gradle/libs.versions.toml file is used by a Gradle 8.x project. When the build syncs, the retrofit dependency fails to resolve — Gradle reports it cannot find com.squareup.retrofit2:retrofit:retrofit. The other two dependencies resolve correctly. Identify the single line that contains the bug.#
[versions]
retrofit = "2.9.0"
okhttp = "4.12.0"
[libraries]
retrofit = { group = "com.squareup.retrofit2", name = "retrofit", version = "retrofit" }
retrofit-converter-gson = { group = "com.squareup.retrofit2", name = "converter-gson", version.ref = "retrofit" }
okhttp-logging = { group = "com.squareup.okhttp3", name = "logging-interceptor", version.ref = "okhttp" }Show answer
The bug is on line 6.
On line 6, version = "retrofit" sets the version to the literal string "retrofit", so Gradle looks for com.squareup.retrofit2:retrofit:retrofit, which does not exist. To reference the version declared in the [versions] table ("2.9.0"), the key must be version.ref = "retrofit", as correctly done on line 7 for retrofit-converter-gson and line 8 for okhttp-logging. In a Gradle version catalog TOML, version = "…" sets a hardcoded literal, while version.ref = "…" dereferences an entry from [versions].
Android/background-work/services-vs-workers
True or False: A Service's lifecycle callbacks (such as onCreate and onStartCommand) run on the main thread, and a CoroutineWorker's doWork() method also runs on the main thread by default.#
Options
Show answer
False. A Service's lifecycle callbacks (onCreate, onStartCommand) run on the main thread, but a CoroutineWorker's doWork() executes on a background dispatcher (Dispatchers.Default) by default. WorkManager manages background threading for you, whereas a Service requires you to manually offload long-running work to avoid blocking the main thread.
Service lifecycle callbacks (onCreate, onStartCommand, onDestroy) do execute on the main thread, which is why long-running work in a Service must be offloaded to a background thread. However, a CoroutineWorker's doWork() runs on a background thread by default (Dispatchers.Default), not the main thread. This is a key difference: WorkManager handles threading for you, whereas a Service does not.
Android/performance/memory-leaks-lifecycle
Users report that after navigating through several screens and rotating the device a few times, the app's memory usage keeps climbing and old Activities never seem to get garbage collected. Which line is the root cause?#
1| object AnalyticsHolder {
2| private var currentActivity: Activity? = null
3|
4| fun attach(activity: Activity) {
5| currentActivity = activity
6| }
7|
8| fun trackScreenView(name: String) {
9| currentActivity?.let { logEvent(name, it.localClassName) }
10| }
11| }Options
Show answer
Line 2 — a singleton (object) holds a var reference to an Activity, which is a GC root that outlives any individual Activity instance; every attach() call leaks the previously-held Activity (and its whole View tree) for as long as the singleton exists, i.e. the lifetime of the process
A Kotlin object is a singleton that lives for the process's lifetime — it's exactly the kind of long-lived GC root Android developers are warned about, and storing a mutable reference to an Activity inside one is a textbook leak: nothing ever clears currentActivity when that Activity is destroyed (on rotation, on back-press, on navigating away), so the singleton keeps pinning the old Activity — and everything it transitively references (its whole View hierarchy, any Contexts derived from it) — in memory indefinitely, while the system believes it destroyed that Activity. Every subsequent attach() call makes it worse by leaking the previous Activity permanently (nothing frees it, attach() only overwrites the reference for future lookups) and the one that's live now stays pinned too. The fix is to never hold a long-lived reference to an Activity/View/Context tied to a UI lifecycle; if a singleton genuinely needs a Context, it should hold applicationContext, which is safe because it's already tied to the process lifetime. ?.let (b) and a parameter's name (c) have no bearing on garbage collection, and Kotlin singletons are not specially collected alongside any particular Activity (d) — GC only reclaims what has no remaining reachable reference, which is exactly what line 2 prevents.
Android/data-persistence/dao-entities-migrations
A release adds a new non-null column to an @Entity and bumps the @Database version. QA reports that every existing user's app crashes on first launch after updating. Which line is the bug?#
1| @Database(entities = [Order::class], version = 2, exportSchema = true)
2| abstract class AppDatabase : RoomDatabase() {
3| abstract fun orderDao(): OrderDao
4|
5| companion object {
6| fun build(context: Context): AppDatabase =
7| Room.databaseBuilder(context, AppDatabase::class.java, "orders.db")
8| .build()
9| }
10| }Options
Show answer
Lines 6–8 — the builder bumps version to 2 but registers no Migration(1, 2) and doesn't call fallbackToDestructiveMigration() either, so Room finds an existing version-1 database on disk with no path to reconcile it and throws an IllegalStateException at open time
Room treats a version bump as a promise that the developer has told it how to get from the old schema to the new one. When it opens an existing database file whose stored version (1) doesn't match the code's declared version (2), it looks for a registered Migration(1, 2) to run the necessary ALTER TABLE/CREATE/COPY SQL; finding none, and with no fallbackToDestructiveMigration() opted into either, its only safe option is to refuse to open the mismatched database and throw — silently guessing at a schema reconciliation would risk corrupting or misreading user data. That's exactly what happens to every existing installed user here: a fresh install never hits this (there's no old file to reconcile), which is why it's easy to miss in dev testing on a clean emulator and only surfaces against real upgraded users, exactly as QA reported. The fix is either a real Migration(1, 2) object that adds the new column, or, if data loss is acceptable, an explicit fallbackToDestructiveMigration() call. exportSchema (b) is a schema-history feature with no crash implication, abstract fun in a RoomDatabase subclass is the required, correct pattern (Room generates the implementation) so (c) is backwards, and Room never migrates automatically with no instructions regardless of default values (d).
Android/background-work/services-vs-workers
A senior engineer is choosing among a plain coroutine launched in a ViewModel, a foreground Service, and WorkManager for a piece of background work. What's the deciding question for each, and when is each the right choice?#
Show answer
A plain coroutine scoped to a ViewModel (viewModelScope) is right for work tied to the UI being on screen and short-lived enough that it's fine for it to simply stop if the user navigates away or the process dies — it has no persistence and no guarantee of completion beyond the current session. A foreground Service is right for work the user needs to be actively aware is happening right now and that should keep running even if the user leaves the current screen within the same app session — media playback, an active navigation session, an ongoing file transfer — and it requires a persistent notification specifically so the user knows work is running on their behalf. WorkManager is right for deferrable work that must eventually complete even across process death, app close, or a device reboot, and that can tolerate running at a system-chosen time subject to constraints (network, charging, idle) — periodic sync, analytics upload, log flushing. The deciding questions are: does this need to survive the current UI session (rules out plain coroutine), does the user need active visibility that it's running right now (Service), and does it need to be guaranteed even across process death with no user awareness required (WorkManager).
This is a genuinely common senior-level design question because all three tools can technically run the same code, so the right answer is about the guarantee each one gives, not the code inside it. A coroutine in viewModelScope gives no guarantee beyond the current UI session — it's cancelled automatically when the ViewModel is cleared, which is correct for work that should stop with the screen. A foreground Service is for work the OS and user both need active, real-time visibility into (hence the mandatory notification), and it still dies with the process, so it's not a durability guarantee either. WorkManager is the only one of the three that persists its own scheduling state to disk and survives process death and reboots, which is exactly the guarantee deferrable, must-eventually-happen work needs — picking the wrong one of these three is a frequent source of real production bugs (uploads silently dropped when the user backgrounds the app, for instance).
Android/background-work/workmanager
A OneTimeWorkRequest is enqueued with a BackoffPolicy.LINEAR (initial delay 10 s). On the first execution its doWork() returns Result.retry(); on the second execution it returns Result.success(). What is the correct chronological order of WorkInfo.State values this work passes through, from the moment it is enqueued to the moment it reaches a terminal state?#
Put these in order
Show answer
A OneTimeWorkRequest that retries once passes through five WorkInfo.State values: ENQUEUED → RUNNING → ENQUEUED → RUNNING → SUCCEEDED. After doWork() returns Result.retry(), WorkManager reschedules the work with the backoff delay, so the state returns to ENQUEUED before the second execution attempt runs and ultimately reaches SUCCEEDED.
When a OneTimeWorkRequest is enqueued it enters ENQUEUED. Once constraints are satisfied the Worker is dispatched and the state becomes RUNNING. When doWork() returns Result.retry(), WorkManager reschedules the work (applying the configured backoff delay) and the state returns to ENQUEUED. On the second attempt the state becomes RUNNING again, and when doWork() returns Result.success() the work transitions to SUCCEEDED. The full sequence is: ENQUEUED → RUNNING → ENQUEUED → RUNNING → SUCCEEDED.
Android/background-work/workmanager
You call WorkManager.getInstance(ctx).enqueueUniqueWork("sync", ExistingWorkPolicy.REPLACE, newRequest) while a previously enqueued work with unique name "sync" is currently in the RUNNING state. The new request has no unmet constraints and its doWork() will return Result.success().#
Put these in order
Show answer
With ExistingWorkPolicy.REPLACE, the existing work is cancelled first (transitioning to CANCELLED), and only then is the new work enqueued. The new work then proceeds through its normal lifecycle: ENQUEUED → RUNNING → SUCCEEDED. The old work's cancellation always precedes the replacement's enqueue because the policy mandates cancel-then-enqueue semantics.
ExistingWorkPolicy.REPLACE specifies that existing pending (uncompleted) work with the same unique name is cancelled first, then the new work is enqueued. The old RUNNING work transitions to CANCELLED. Only after cancellation does the new work enter ENQUEUED. The new work then proceeds through its normal lifecycle: ENQUEUED → RUNNING → SUCCEEDED (since doWork() returns success). The old work's CANCELLED transition strictly precedes the new work's ENQUEUED transition.
Android/intents-navigation/intent-filters
An Android app fires the following implicit intent to start another activity:#
Options
Pick every one that applies.
Show answer
Filters A and E match the implicit ACTION_VIEW intent with a content:// URI. Filter A satisfies all three tests: matching action, CATEGORY_DEFAULT (which the framework auto-adds to every implicit startActivity intent), and matching content scheme. Filter E also matches because extra categories in a filter (like BROWSABLE) don't disqualify it—the filter only needs to contain every category present in the intent. Filters B, C, and D fail due to a missing DEFAULT category, a scheme mismatch (https vs content), and an action mismatch (EDIT vs VIEW), respectively.
Intent resolution for an implicit intent tests action, category, and data in sequence.
The intent's action is ACTION_VIEW; its data URI scheme is content. The framework auto-adds CATEGORY_DEFAULT, so the intent's category set is exactly {DEFAULT}.
Filter A — Action VIEW matches. The filter declares CATEGORY_DEFAULT, satisfying the category test (the filter must contain every category present in the intent). The data scheme content matches the intent's content:// URI. Match.
Filter B — The filter omits CATEGORY_DEFAULT. Because startActivity() injects CATEGORY_DEFAULT into the implicit intent, the category test fails: every category in the intent must be listed in the filter. No match.
Filter C — Action and category pass, but the filter's data scheme is https, which does not match the intent's content scheme. No match.
Filter D — The filter's action is EDIT, not VIEW. The action test requires at least one filter action to match the intent's action. No match.
Filter E — Action VIEW matches. The filter declares both DEFAULT and BROWSABLE; the category test only requires that every category in the intent appear in the filter—extra categories in the filter are irrelevant. DEFAULT is present, so the test passes. The scheme content matches. Match.
Therefore Filters A and E match; B, C, and D do not.
Android/background-work/services-vs-workers
A junior engineer argues that a started Service and a WorkManager OneTimeWorkRequest both "run in the background" and are therefore interchangeable. Identify the two critical execution guarantees that WorkManager provides but a plain started Service does not, and name the on-device mechanism WorkManager uses to achieve persistence. Be specific about what survives (or does not survive) process death and device reboot.#
Show answer
WorkManager guarantees (1) persistence across process death and device reboot, and (2) guaranteed eventual execution — the system retries per the configured backoff policy until the Worker returns Success, Failure, or is cancelled. A plain started Service's state lives only in memory; if the process is killed the Service is gone. Even with START_STICKY the system restarts the Service with a null intent, so the original work payload is lost. WorkManager achieves persistence by serializing each WorkSpec (input data, constraints, backoff policy, state) into a Room database and replaying pending work after BOOT_COMPLETED via a BroadcastReceiver registered for that intent.
WorkManager's two distinguishing guarantees are crash-safe persistence (via a Room-backed WorkSpec table) and guaranteed eventual execution with backoff. A started Service provides neither: its in-memory state is lost on process death, and START_STICKY only restarts with a null intent. This is the fundamental architectural difference between the two APIs.
Related interview questions
The other 71 questions
This page shows 25. A free account opens the rest and marks your answers, which a page of answers can't do. Miss one and it returns a few days later, then a week after that, until you stop missing it.
Free · the whole bank · 100 graded answers per 30 days · written feedback and job-ad quizzes on the paid plan