Android interviews focus on the platform behaviours that catch developers out. Expect questions on the Activity lifecycle and configuration changes, why ViewModel exists and what it does not survive, background work and why WorkManager replaced Services, coroutines and dispatchers, StateFlow versus LiveData, Jetpack Compose and recomposition, runtime permissions, and Room. Performance, memory leaks and device fragmentation are recurring themes. The questions below cover the framework and production concerns.
Behavioural Questions
1. Tell me about an Android app you built or worked on. What was your role?
Note: Mobile interviews reward specifics about users and constraints. Play Store link, download numbers, or crash-free rate all make an answer concrete.
Cover:
- What the app did and its scale. Downloads, active users, and the range of devices and Android versions you supported. Supporting API 24 upwards is a very different job from targeting only recent devices.
- Your scope. Whether you owned the architecture, a feature, the network layer, or the release process. Be precise.
- The stack. Java or Kotlin, Views or Jetpack Compose, and the architecture — MVVM, MVI, or something older. Say honestly if it was a legacy codebase.
- The hard part. Strong candidates: background work surviving Doze and manufacturer battery restrictions, offline support and sync conflicts, memory or ANR problems on low-end devices, or fragmentation across OEM behaviour.
- The outcome — crash-free rate, startup time, app size, or rating before and after.
2. How do you handle testing on the huge variety of Android devices and versions?
Show that you approach fragmentation systematically rather than hoping.
- Decide coverage from your own data, not from general statistics. Play Console shows the actual distribution of devices, Android versions, and screen sizes among your users. That tells you what to support and, importantly, what to drop.
- Test the extremes deliberately. The oldest supported API level, the newest, a low-RAM device, a very small screen, and a tablet or foldable. Bugs cluster at the boundaries.
- Test on real devices for the things emulators cannot reproduce — camera, sensors, performance on weak hardware, and manufacturer battery management. Firebase Test Lab gives access to a device matrix in CI.
- Automate what you can. Unit tests for logic, Espresso or Compose tests for UI, and instrumented tests run across a small device matrix on every merge.
- Use staged rollouts. Releasing to 5% first, watching crash-free rate and ANR rate, then expanding, catches device-specific problems before they reach everyone.
Note: OEM-specific behaviour — particularly aggressive background process killing on some manufacturers — is a genuine Android problem that catches out candidates who have only tested on Pixels. Mentioning it signals real production experience.
3. Describe a difficult bug you fixed in an Android app.
Choose a bug that needed investigation, and describe the method.
Strong scenarios:
- A crash affecting only some devices or one manufacturer, found through Crashlytics stack traces and device breakdown.
- A memory leak — an Activity retained by a static reference, a listener never unregistered, or an inner class holding an implicit reference to its outer Activity. LeakCanary and the Android Studio Memory Profiler are the tools.
- An ANR caused by work on the main thread — a database query, a large JSON parse, or disk I/O.
- A configuration change bug where state was lost on rotation or after process death.
- A race condition in asynchronous code that only appeared on slow networks.
How to tell it: how you reproduced it (usually the hardest part), how you narrowed it down, the tools you used, the root cause, and the regression test or structural change that prevents recurrence.
Note: Process death is an excellent thing to mention. Testing with "Don't keep activities" enabled in developer options simulates it, and a large share of Android state bugs only appear that way — most candidates have never tried it.
4. How do you work with designers and product managers on mobile features?
Show that you engage before implementation, since mobile has constraints designers may not think about.
- Review designs early and raise platform realities. Material Design conventions, back navigation behaviour, system bars and gesture insets, keyboard behaviour, and how the layout survives a 320dp-wide screen or a 200% font scale.
- Ask about the states designs usually omit — empty, loading, error, offline, and very long content. On mobile, offline is not an edge case.
- Quantify cost rather than refusing. "That animation is possible but it needs a custom view and about three days; this alternative gets the same effect in half a day" is a conversation.
- Push back on anything that hurts accessibility — touch targets below 48dp, poor contrast, or text that does not scale.
- Get something running early. A rough build on a real device surfaces problems no mockup reveals, because a design that looks good on a laptop can feel wrong in the hand.
Note: Mentioning that you flag when a design fights platform conventions — for example an iOS-style pattern that confuses Android users — shows you advocate for the user rather than just implementing tickets.
5. Android changes significantly with each release. How do you keep current and manage those changes?
How you keep up: the Android developer release notes and behaviour change documentation for each API level, the Android Developers Blog, and Jetpack library release notes. Google I/O sessions give the reasoning behind changes rather than just the rules.
How you manage them — the practical part:
- Read the behaviour changes for the new API level before raising
targetSdkVersion. Google separates changes affecting all apps from those affecting only apps that target the new level, and that distinction determines urgency. - Play Store deadlines force the schedule. Google requires apps to target a recent API level to remain updatable, so this is not optional work and should be planned, not discovered.
- Test against the new version early, during the developer preview if the app is significant.
- Watch the recurring themes — background execution limits, storage access, and permissions have tightened with almost every release, and each has broken apps that were not paying attention.
Note: Distinguish keeping up with the platform from keeping up with libraries. Compose and architecture libraries change fast, but platform behaviour changes are the ones that break shipped apps for real users, so they take priority.
Technical Questions
1. Explain the Android Activity lifecycle and why it matters.
An Activity moves through callbacks as the system creates, shows, hides, and destroys it:
onCreate()— once, when created. Inflate the layout, initialise the ViewModel, restore saved state.onStart()— becoming visible.onResume()— in the foreground and interactive. Start camera, sensors, animations, and location updates here.onPause()— losing focus but possibly still partly visible. Keep this fast; anything slow delays the next Activity appearing.onStop()— no longer visible. Release heavier resources and unregister listeners.onDestroy()— being destroyed, either by finishing or by a configuration change.
Why it matters so much:
- Configuration changes destroy and recreate the Activity by default. Rotating the screen runs the whole cycle again, so anything held only in Activity fields is lost. This is why ViewModel exists — it survives configuration changes.
- Process death is different and harsher. Android can kill a backgrounded app entirely to reclaim memory. ViewModel does not survive that; only
onSaveInstanceStateorSavedStateHandledoes. - Leaks come from lifecycle mismatches — a listener registered in
onCreateand never unregistered keeps the Activity alive.
Note: Lifecycle-aware components and repeatOnLifecycle for collecting flows are the modern answer, because they tie observation to the lifecycle automatically rather than relying on you remembering to unregister.
2. What is the difference between Activity, Fragment and Service?
- Activity — a single screen with a user interface, and an entry point into the app. It has its own lifecycle and appears in the back stack. Modern apps typically use few Activities — often a single one hosting many destinations.
- Fragment — a reusable portion of UI hosted inside an Activity, with its own lifecycle tied to the host's. Fragments exist so that one Activity can present different content — essential for tablets and foldables where two panes appear side by side, and the basis of Navigation Component destinations. The critical subtlety is that a Fragment has two lifecycles: the Fragment itself, and its view, which is destroyed and recreated when the Fragment goes onto the back stack. Observing LiveData with the Fragment's lifecycle rather than
viewLifecycleOwneris a classic bug that causes duplicate observers. - Service — a component for work with no user interface, running in the background. Foreground services show a persistent notification and are for user-visible ongoing work such as music playback or navigation. Bound services provide an interface for other components to call.
Note: The important modern point about Services: background execution limits since Android 8 mean a plain background Service is no longer a reliable way to do deferred work. WorkManager is the correct API for guaranteed background work, and a foreground service is required for anything long-running and user-visible. Saying you would use a Service for periodic sync is a dated answer.
3. What is an Intent, and what is the difference between explicit and implicit intents?
An Intent is a messaging object describing an operation to be performed. It is how Android components communicate, including across app boundaries.
- An explicit Intent names the exact component to start, by class. Used for navigation within your own app — starting your own Activity or Service. It is unambiguous and secure, because you know exactly what will handle it.
- An implicit Intent declares an action and lets the system find a component that can handle it. Sharing text, opening a URL, dialling a number, or picking an image. The system matches the action, data, and category against the intent filters declared by installed apps, and shows a chooser if several qualify.
// Explicit
startActivity(Intent(this, DetailActivity::class.java))
// Implicit
val intent = Intent(Intent.ACTION_VIEW, Uri.parse(url))Practical points that matter:
- Always handle the case where nothing can handle an implicit Intent, or the app crashes with
ActivityNotFoundException. - Package visibility since Android 11 means you must declare
<queries>in the manifest to see which apps can handle certain intents. - Never send sensitive data in an implicit Intent — any app with a matching filter can receive it.
- PendingIntents must specify mutability (
FLAG_IMMUTABLEorFLAG_MUTABLE) from Android 12, and immutable should be the default.
4. What is MVVM, and how do ViewModel, LiveData and StateFlow fit together?
MVVM separates concerns into three layers:
- Model — data and business logic, usually behind a Repository that decides between network and local cache.
- View — the Activity, Fragment, or Composable. It observes state and forwards user events. It should contain no business logic.
- ViewModel — holds and exposes UI state, and survives configuration changes. It must never reference a View or Context that could leak an Activity.
The observable holders:
- LiveData — lifecycle-aware by design. It only emits to active observers and cleans up automatically, which is why it was safe before coroutines matured. It always holds a current value.
- StateFlow — the Kotlin coroutines equivalent, holding a current value and emitting updates. It is not lifecycle-aware by itself, so it must be collected inside
repeatOnLifecycle(STARTED), or withcollectAsStateWithLifecycle()in Compose. Otherwise collection continues while the app is backgrounded, wasting work. - SharedFlow — for one-off events such as showing a snackbar or navigating, where a state holder would replay the event after rotation.
Note: StateFlow is the modern recommendation because it composes with the rest of the coroutines API — operators, combining streams, and structured concurrency — while LiveData does not. The single-event problem is the detail interviewers probe: using StateFlow for navigation events causes the navigation to repeat on rotation.
5. What is Jetpack Compose and how does it differ from the View system?
Jetpack Compose is Android's declarative UI toolkit. You describe what the UI should look like for a given state, and the framework updates it when the state changes.
The differences from Views:
- Declarative versus imperative. With Views you find a widget and mutate it —
textView.text = name. With Compose you write a function of state, and recomposition handles updating. This removes a whole class of bug where the UI and the underlying state drift apart. - No XML. Layouts are Kotlin, so you get type safety, loops, conditionals, and normal refactoring tools.
- Composition over inheritance. Reuse comes from small composable functions rather than subclassing View.
- State is explicit.
rememberholds state across recompositions;rememberSaveablealso survives configuration changes and process death.
Recomposition is the key concept: when state a composable reads changes, Compose re-runs that function. It is intelligent — only affected composables re-run — but composables must therefore be side-effect free and cheap, since they may run often and in any order. Side effects belong in LaunchedEffect, DisposableEffect, or similar.
Note: Two practical points. Compose and Views interoperate, so migration can be incremental via ComposeView and AndroidView. And state hoisting — moving state up so a composable takes state and a callback rather than owning it — is the pattern that makes composables reusable and testable.
6. How do you handle background work and threading in Android?
The main thread draws the UI. Blocking it for more than a few hundred milliseconds causes jank; blocking it for five seconds causes an ANR. Network calls, database queries, and file I/O must never run there.
Coroutines are the modern answer:
viewModelScopeties work to the ViewModel and cancels it automatically when the ViewModel clears.lifecycleScopeties work to an Activity or Fragment lifecycle.- Dispatchers choose the thread pool —
Dispatchers.IOfor network and disk,Dispatchers.Defaultfor CPU-bound work,Dispatchers.Mainfor UI.withContextswitches between them cleanly. - Structured concurrency means cancelling a scope cancels its children, which prevents leaked work.
For deferrable, guaranteed work — WorkManager. This is the important distinction. Work that must complete even if the app is closed or the device restarts — uploading a file, periodic sync, sending analytics — belongs in WorkManager, which handles constraints (network available, charging), retries with backoff, and chaining. It respects Doze and battery optimisation rather than fighting them.
What not to use: AsyncTask is deprecated and leaks Activities; raw Thread gives no lifecycle awareness; and a plain background Service is unreliable under background execution limits since Android 8.
Note: Manufacturer battery optimisation aggressively kills background work on some devices. Mentioning it — and that you would use WorkManager and expedited work rather than assuming a Service stays alive — signals real experience.
7. How do you store data in an Android app, and what is Room?
The options, matched to the use case:
- DataStore — key-value or typed proto storage for small data such as user preferences and flags. It replaces
SharedPreferences, which had a synchronous API that could block the main thread and no error signalling. DataStore is asynchronous, coroutine and Flow based, and transactional. - Room — an abstraction over SQLite for structured relational data. It is the standard answer for anything queryable.
- Internal storage — files private to the app, deleted on uninstall.
- MediaStore and the Storage Access Framework — for shared media and user-selected documents, since scoped storage restricted direct filesystem access from Android 10.
- EncryptedSharedPreferences or the Keystore — for tokens and sensitive values. Credentials should never sit in plain preferences.
Room specifically has three parts: Entity classes annotated to define tables, a DAO interface declaring queries, and a Database class tying them together. Its main advantage is that SQL is verified at compile time — a typo in a query is a build error rather than a runtime crash, which raw SQLite could not offer. It also returns Flow, so the UI updates automatically when the underlying data changes, and integrates with coroutines so queries run off the main thread.
Note: Migrations are the practical difficulty. Changing a schema requires a Migration, and falling back to destructive migration wipes user data — acceptable in development, never in production.
8. How do Android permissions work, and what changed with runtime permissions?
Permissions are declared in the manifest, but how they are granted depends on their protection level.
- Normal permissions — low risk, such as internet access or vibration. Granted automatically at install; the user is never asked.
- Dangerous permissions — access to private data or sensitive hardware: camera, location, contacts, microphone, and certain storage access. Since Android 6 (API 23) these must be requested at runtime, and the user can deny or later revoke them.
- Signature permissions — granted only to apps signed with the same certificate.
The runtime flow: check with ContextCompat.checkSelfPermission, request through the Activity Result API, and handle the result. Critically, you must handle all three outcomes — granted, denied, and permanently denied — and the app must remain usable when a permission is refused. Requesting everything at launch is the pattern most likely to get an app uninstalled.
What tightened since:
- Android 10 — background location became a separate, harder-to-obtain permission, and scoped storage restricted filesystem access.
- Android 11 — one-time permissions, and auto-revocation for unused apps.
- Android 13 — granular media permissions replacing broad storage access, and a runtime permission for notifications.
- Android 14 — partial photo and video access, letting users share only selected items.
Note: Explain why you need a permission in context, immediately before requesting it. Requesting without context is the main cause of denial.
9. How do you improve the performance of an Android app?
Measure first with Android Studio Profiler for CPU, memory, and network; Macrobenchmark for startup and scrolling; and Play Console vitals for real-world ANR and crash rates on real devices, which is what actually matters.
Startup time:
- Do as little as possible in
Application.onCreate. Third-party SDK initialisation is the usual culprit — use App Startup or lazy initialisation. - Avoid disk and network on the startup path.
- Baseline Profiles ship ahead-of-time compilation hints and give a substantial, low-effort improvement to startup and scrolling.
Rendering and jank:
- Keep the main thread free — no I/O, no heavy parsing.
- Flatten deep view hierarchies; use ConstraintLayout, or Compose with stable parameters to limit recomposition.
- Use paging for long lists rather than loading everything.
Memory:
- Fix leaks with LeakCanary — retained Activities are the most common.
- Load images at display size with Coil or Glide rather than full resolution bitmaps, which is the single largest source of out-of-memory crashes.
App size: enable R8 shrinking and obfuscation, ship an Android App Bundle so users download only what their device needs, use WebP or vector drawables, and audit dependencies.
Note: Test on a low-end device. An app that feels fine on a flagship can be unusable on the hardware much of the market actually uses.
10. What is dependency injection in Android, and why use Hilt or Dagger?
Dependency injection means a class receives its dependencies from outside rather than constructing them. Instead of a ViewModel creating its own Repository, which creates its own API client, the dependencies are supplied.
Why it matters on Android specifically:
- Testability. A ViewModel that constructs a real network client cannot be unit tested. One that receives a Repository interface can be given a fake. This is the main argument.
- Lifecycle-correct scoping. Some objects should be singletons for the app's life — a database, an OkHttp client — while others should live only as long as a screen. A DI framework enforces that rather than leaving it to static fields, which leak.
- Less boilerplate. Without it, every object graph must be wired by hand, and changing a constructor means editing every call site.
Hilt is built on Dagger and is the recommended option for Android. It generates the code at compile time, so errors surface as build failures rather than runtime crashes, and it has no reflection overhead. It provides predefined components matching Android lifecycles — SingletonComponent, ViewModelComponent, ActivityComponent — with annotations such as @HiltAndroidApp, @AndroidEntryPoint, and @Inject.
Note: Koin is the common alternative — simpler to learn and Kotlin-idiomatic, but it resolves at runtime, so a missing binding is a crash rather than a compile error. That trade-off is the honest comparison to draw.





