Skip to content

Hard Android App Development Interview Questions

41 hard questions with answers. Design, trade-off and deep-dive questions asked for senior roles and at top product companies.

Topics in the hard set

Coroutines and Flow: 8 Testing, Performance and Play Store: 8 Networking and APIs: 6 Room, DataStore and WorkManager: 5 Android Components and Lifecycle: 4 Jetpack Compose: 4 Kotlin Fundamentals: 3 App Architecture, MVVM and Hilt: 3
Solved — hardStart practicing
3 QUESTIONS FREESign up to unlock the full question bank and track your progress.Unlock every question and answer, then continue practising company-wise and package-wise.
3 questions free Unlock the rest in seconds
01

What does the inline keyword do and when should you use reified?

Save to ▾
Show answer
inline tells the compiler to copy the function body and lambda bodies into the call site, avoiding the object allocation for lambdas and enabling non-local returns. It suits small higher-order functions called often. reified can only be used in inline functions and keeps the generic type available at runtime, so you can write inline fun <reified T> Gson.fromJson(json: String) and use T::class. Avoid inlining large functions since it increases bytecode size.
02

Explain generics variance with in and out in Kotlin.

Save to ▾
Show answer
out marks a type parameter as covariant: the class only produces T, so List<String> can be used as List<out Any>. in marks it contravariant: the class only consumes T, like Comparator<in T>. This is declaration-site variance, replacing Java wildcards such as ? extends and ? super. Use-site projections like Array<out Number> are also possible. Getting variance right lets APIs accept more types safely without unchecked casts.
03

What are value classes and why use them?

Save to ▾
Show answer
A value class, declared with @JvmInline value class UserId(val value: String), wraps a single value to give it a distinct type without the runtime cost of an object in most cases, because the compiler unboxes it. It prevents mistakes like passing an OrderId where a UserId is expected. It may get boxed when used as a nullable type, in generics or through interfaces, so the zero-cost benefit is not guaranteed everywhere.
04

How does coroutine cancellation work, and why must code be cooperative?

Save to ▾
Show answer
Cancelling a Job sets it to cancelling state, and the next suspension point such as delay, withContext or a suspending library call throws CancellationException. A tight CPU loop with no suspension keeps running, so check isActive or call ensureActive() or yield() inside it. Do not swallow CancellationException in a generic catch block; rethrow it. Use finally or withContext(NonCancellable) for cleanup that must finish.
05

What is the difference between Job and SupervisorJob?

Save to ▾
Show answer
With a regular Job, a failing child cancels its parent and all siblings. With a SupervisorJob, or inside supervisorScope, a child failure does not affect siblings or the parent; each child handles its own errors. viewModelScope uses a SupervisorJob, so one failed launch does not kill other work in the ViewModel. Use supervisor behaviour when tasks are independent, like loading separate widgets on a dashboard.
06

Write a search flow that debounces user input and cancels stale requests.

Save to ▾
Show answer
private val query = MutableStateFlow("") val results: StateFlow<List<Item>> = query .debounce(300) .map { it.trim() } .filter { it.length >= 2 } .distinctUntilChanged() .flatMapLatest { q -> repo.search(q) } .catch { emit(emptyList()) } .stateIn(viewModelScope, SharingStarted.WhileSubscribed(5000), emptyList()) fun onQueryChange(text: String) { query.value = text }
07

What do flatMapLatest, flatMapConcat and flatMapMerge do?

Save to ▾
Show answer
All map each value to a new flow and flatten the results. flatMapLatest cancels the previous inner flow when a new value arrives, perfect for search. flatMapConcat waits for each inner flow to finish before starting the next, preserving order. flatMapMerge runs inner flows concurrently up to a concurrency limit, useful for parallel downloads where order does not matter. Choosing wrongly can cause stale results or wasted network calls.
08

Write a suspend function that wraps a callback-based API.

Save to ▾
Show answer
suspend fun FusedLocationProviderClient.awaitLastLocation(): Location? = suspendCancellableCoroutine { cont -> lastLocation .addOnSuccessListener { cont.resume(it) } .addOnFailureListener { cont.resumeWithException(it) } cont.invokeOnCancellation { /* cancel request if supported */ } } // For streams of callbacks, use callbackFlow { } with awaitClose { }.
09

How do you unit test a ViewModel that uses coroutines and StateFlow?

Save to ▾
Show answer
Use kotlinx-coroutines-test. Replace Dispatchers.Main with a test dispatcher using Dispatchers.setMain in a JUnit rule, run tests in runTest so delays are skipped with virtual time, and inject dispatchers into repositories instead of hardcoding Dispatchers.IO. For flows, collect values with Turbine or backgroundScope and assert the sequence, such as Loading then Success. Fake repositories make results deterministic.
10

You need to sync 500 images to a server without overloading the network. How would you design it with coroutines?

Save to ▾
Show answer
Limit concurrency. Use a Semaphore with withPermit around each upload, or flatMapMerge(concurrency = 4) over a flow of files, or Dispatchers.IO.limitedParallelism(4). Use supervisorScope so one failed upload does not cancel the rest, collect failures for retry with backoff, and report progress through a StateFlow. If the sync must survive app exit, run it inside a CoroutineWorker with WorkManager constraints such as network connected.
11

Explain backpressure handling in Flow with buffer, conflate and collectLatest.

Save to ▾
Show answer
Flow is sequential by default: the producer suspends until the collector processes each value. buffer() runs producer and collector in separate coroutines with a queue between them. conflate() keeps only the latest value when the collector is slow, dropping intermediate ones, good for sensor or progress updates. collectLatest cancels processing of the previous value when a new one arrives. Choose based on whether every value matters.
12

A crash says IllegalStateException: Can not perform this action after onSaveInstanceState. What is happening?

Save to ▾
Show answer
A fragment transaction or dialog show was committed after the Activity saved its state, usually because a network callback finished while the app was in background. Committing then would lose state. Fix by triggering navigation only when the lifecycle is at least STARTED, collecting events with repeatOnLifecycle, or deferring the action. commitAllowingStateLoss hides the crash but can lose the transaction, so use it only when that is acceptable.
13

The product team wants location tracking for delivery partners even when the app is closed. How do you implement it?

Save to ▾
Show answer
Use a foreground service with foregroundServiceType location, a visible notification, and the FusedLocationProviderClient with a sensible interval. Request ACCESS_FINE_LOCATION first, then ACCESS_BACKGROUND_LOCATION separately where truly needed, explaining why. Declare foreground service permissions for Android 14. Batch and upload locations efficiently, handle battery optimisation, and complete the Play Console location permissions declaration, since background location is reviewed strictly.
14

How does Android handle background execution limits and Doze mode?

Save to ▾
Show answer
Doze defers network access, jobs, alarms and syncs when the device is idle, with short maintenance windows. App Standby Buckets limit how often apps run based on usage. Since Android 8, background services are restricted, and since Android 12 apps cannot start foreground services from background except in specific cases. Use WorkManager for deferrable work, FCM high-priority messages for urgent pushes, and exact alarms only when truly required.
15

What is the predictive back gesture and how do you support it?

Save to ▾
Show answer
Predictive back, introduced in Android 13 and on by default for apps targeting Android 15, shows a preview of the destination while the user swipes back. Support it by setting android:enableOnBackInvokedCallback="true" and handling back with OnBackPressedDispatcher or BackHandler in Compose instead of overriding onBackPressed, which is deprecated. Navigation libraries and Material components provide animations when you use the supported APIs.
16

What are stable and unstable types in Compose?

Save to ▾
Show answer
Compose skips recomposing a composable when all its parameters are stable and equal to before. Primitives, String, functions and data classes with val stable properties are stable. Classes with var properties, standard List or types from modules without the Compose compiler may be unstable, preventing skipping. Since strong skipping mode became default in Kotlin 2.0.20, unstable parameters are compared by instance, reducing the problem, but immutable models still help.
17

A Compose list scrolls with jank on low-end phones. How do you investigate and fix it?

Save to ▾
Show answer
Test a release build with R8, since debug builds are much slower. Use Layout Inspector recomposition counts and a system trace or Macrobenchmark to find hot spots. Common fixes: add keys and contentType, avoid heavy work or sorting inside composables, move computation to the ViewModel, use derivedStateOf for scroll-based state, defer state reads with lambda modifiers like offset { }, load images at the right size with Coil, and add a Baseline Profile.
18

How do you build an adaptive layout for phones, foldables and tablets in Compose?

Save to ▾
Show answer
Use WindowSizeClass from the Material 3 adaptive library to choose layouts by width class, not device type. On compact width show a bottom navigation bar; on medium use a navigation rail; on expanded use a list-detail layout with ListDetailPaneScaffold or NavigationSuiteScaffold. Avoid locking orientation, since Android 16 ignores orientation restrictions on large screens. Test with resizable emulators and foldable postures.
19

Explain the three phases of a Compose frame.

Save to ▾
Show answer
Each frame goes through composition, which decides what UI to show by running composables; layout, which measures and places each element; and drawing, which renders pixels. State read in a phase only invalidates that phase and later ones. Reading scroll state inside Modifier.offset { } or drawBehind defers the read to layout or draw, skipping recomposition entirely, which is a key optimisation for animations and scrolling effects.
20

How do you structure a large Android app into modules?

Save to ▾
Show answer
Common practice is feature modules (feature:cart, feature:profile), core modules (core:network, core:database, core:designsystem, core:model) and an app module that wires them together. Feature modules depend on core modules but not on each other; navigation is connected through the app module or route contracts. Benefits are faster incremental builds, clear ownership and enforced boundaries. Gradle convention plugins keep module build files consistent.
21

Product wants a feature flag to turn a new checkout on or off without releasing an update. How do you design it?

Save to ▾
Show answer
Use a remote config service with defaults bundled in the app, wrapped in a FeatureFlags interface injected via Hilt. Fetch and cache values at startup with a sensible timeout, and read the flag at navigation decision points rather than deep inside UI code. Keep both checkout paths tested, log which variant users see for analytics, and plan to remove the old path and the flag after rollout.
22

How would you lead an architecture decision when senior developers disagree?

Save to ▾
Show answer
I would write a short decision document stating the problem, two or three options with trade-offs, and constraints such as team skill and timeline, then build a small spike of the top options on a real screen. We review with measurable criteria like test effort and build time, decide, and record it as an architecture decision record. Once decided, everyone commits, and we revisit only if new facts appear.
23

How do you store sensitive data such as tokens securely on Android?

Save to ▾
Show answer
Never store secrets in plain SharedPreferences or hardcode them in code. Use the Android Keystore to generate a key that never leaves secure hardware, and encrypt the data before storing it in DataStore or a file. The security-crypto EncryptedSharedPreferences library is deprecated, so many teams use Tink with Keystore. Keep tokens short-lived with refresh tokens, and clear them on logout. Exclude them from backups.
24

What is the Paging 3 library and how does it work with Room?

Save to ▾
Show answer
Paging 3 loads large datasets in pages to save memory and network. A PagingSource loads pages, Pager exposes Flow<PagingData>, and the UI uses LazyPagingItems in Compose or PagingDataAdapter in RecyclerView. With a RemoteMediator, the network fills Room and Room acts as the PagingSource, giving offline support and a single source of truth. Use cachedIn(viewModelScope) to survive configuration changes.
25

How would you design an offline-first notes app?

Save to ▾
Show answer
Room is the single source of truth: the UI observes DAO flows and writes go to Room immediately with a sync status column like pending. A WorkManager job uploads pending changes when the network is available and pulls server changes, updating Room. Handle conflicts with timestamps or server versions, use unique work to avoid duplicate syncs, and show a small sync indicator. The app works fully without network.
26

A WorkManager job is not running on some phones from certain manufacturers. How do you investigate?

Save to ▾
Show answer
Some manufacturers apply aggressive battery optimisations that delay or kill background work. Check the job state with WorkManager's getWorkInfosForUniqueWorkFlow and the background task inspector in Android Studio. Verify constraints are not too strict. Guide users to allow unrestricted battery for the app only where the feature truly needs it, use expedited work for urgent tasks, and use FCM to trigger sync when timely delivery matters.
27

Explain how you would use Room with Kotlin Multiplatform.

Save to ▾
Show answer
Since Room 2.7, Room supports Kotlin Multiplatform for Android, iOS and desktop. You define entities, DAOs and the database in commonMain, use the bundled SQLite driver, and create the database builder in platform-specific source sets with expect and actual. DAO functions must be suspend or return Flow in common code. This lets teams share the data layer while keeping native UIs, which some Indian product companies are adopting.
28

How do you refresh an expired access token automatically?

Save to ▾
Show answer
Use an OkHttp Authenticator, which is called when the server returns 401. Inside it, synchronise so only one refresh runs at a time, call the refresh endpoint with the refresh token, save the new tokens and return the original request with the new header. If the request already carried the latest token and still failed, or refresh fails, return null and log the user out. Limit retries to avoid loops.
29

How does certificate pinning work and what are its risks?

Save to ▾
Show answer
Certificate pinning makes the app trust only specific public key hashes for your domain, using OkHttp CertificatePinner or the Network Security Config pin-set. It protects against attackers with a rogue or user-installed CA intercepting traffic. The risk is that if the server certificate changes and the pin does not match, all users lose connectivity until they update. Always pin a backup key and set an expiration for config-based pins.
30

Your app shows duplicate orders when users tap Pay twice on a slow network. How do you fix it?

Save to ▾
Show answer
Disable the button and show progress once tapped, but do not rely only on the UI. Generate an idempotency key on the client for each checkout attempt and send it with the request so the server returns the same result for retries. Ensure the payment call is not retried automatically by OkHttp for non-idempotent POSTs, and let the user check order status if the response is lost.
31

How would you implement real-time order tracking updates in an app?

Save to ▾
Show answer
Options are polling, WebSockets, Server-Sent Events, or push notifications. For a live tracking screen, a WebSocket via OkHttp exposed as a callbackFlow works well, started when the screen is visible and closed when it leaves. Add reconnection with backoff, heartbeat pings, and fall back to polling on failure. When the app is in background, rely on FCM pushes for status changes instead of keeping the socket open.
32

The API team wants to switch to GraphQL. What changes on the Android side?

Save to ▾
Show answer
You would use a client like Apollo Kotlin, which generates type-safe models from the schema and queries at build time, and supports normalised caching and coroutines. Benefits are fetching exactly the fields a screen needs and fewer round trips. Costs include learning the tooling, more complex caching and error handling since errors can come with partial data. I would pilot it on one feature before migrating everything.
33

How do you design an Android client to support API versioning and forced updates?

Save to ▾
Show answer
Send the app version in a header on every request, have the backend support older versions for a defined period, and use versioned endpoints for breaking changes. Add a remote config value for minimum supported version and use the Play In-App Updates API with immediate flow for critical updates and flexible flow otherwise. Show a clear message rather than failing silently when the app is too old.
34

The app's cold start takes 4 seconds. How do you improve it?

Save to ▾
Show answer
Measure first with Macrobenchmark StartupTimingMetric on a release build. Then reduce work in Application.onCreate by deferring SDK initialisation, using App Startup or lazy injection, and moving disk I/O off the main thread. Simplify the first screen, avoid blocking on network before showing UI, use the SplashScreen API properly, and add a Baseline Profile, which often improves startup by about 20 to 30 percent.
35

What are Baseline Profiles?

Save to ▾
Show answer
A Baseline Profile is a list of classes and methods used in critical journeys like startup and scrolling, shipped with the app. The Android Runtime uses it to compile that code ahead of time on install, avoiding slow interpretation and JIT on first runs. You generate it with the Baseline Profile Gradle plugin and Macrobenchmark tests. Compose and many Jetpack libraries ship their own profiles, but an app-specific one gives larger gains.
36

Your banking app must pass a security audit. What would you check on the Android side?

Save to ▾
Show answer
Check secure storage with Keystore and no secrets in code, TLS only with pinning where justified, no sensitive data in logs, FLAG_SECURE on sensitive screens, root and tamper checks using Play Integrity, exported components reviewed, WebView with JavaScript bridges restricted, R8 obfuscation, session timeout, and biometric authentication with BiometricPrompt and CryptoObject. Use the OWASP MASVS as the checklist and fix findings by severity.
37

How would you roll out a risky new feature to millions of users safely?

Save to ▾
Show answer
Ship it behind a remote feature flag defaulted off, release the build with a staged rollout and monitor crash-free and ANR rates. Then enable the flag for internal users, then 1, 5, 25 and 100 percent, watching business metrics and errors at each step. Keep a kill switch ready, define rollback criteria beforehand, and keep the old path until the new one is stable.
38

Users report battery drain after the latest update. How do you investigate?

Save to ▾
Show answer
Check Android vitals for excessive wake locks and wakeups, then use Battery Historian or the Energy Profiler to look at wake locks, alarms, location requests, jobs and network activity. Common causes are frequent location updates, a foreground service left running, polling instead of push, and WorkManager jobs scheduled too often. Fix by batching work, using lower location priority, stopping services promptly and adding proper constraints.
39

Your app has 60 Gradle modules and builds take 15 minutes. How would you speed them up?

Save to ▾
Show answer
Profile with Gradle build scans to find slow tasks. Enable the configuration cache and build cache, including a remote cache in CI. Use KSP instead of kapt, avoid unnecessary api dependencies so changes do not recompile many modules, use convention plugins, and keep non-transitive R classes. Check heavy annotation processors and give Gradle enough memory. Developers can build only the module they work on with a sample app.
40

How do you measure app quality after release?

Save to ▾
Show answer
Track crash-free users and ANR rate in Play vitals and Crashlytics, keeping them below Play's bad behaviour thresholds of 1.09 percent user-perceived crash rate and 0.47 percent ANR rate. Monitor startup time, slow and frozen frames, network error rates and key funnel metrics like login success. Read Play reviews and ratings by version. Set alerts and review these numbers in every release meeting.
41

Describe a time you improved app performance with measurable results.

Save to ▾
Show answer
Situation: our home screen had many frozen frames on budget phones. Task: improve scroll smoothness. Action: I captured a system trace, found image decoding on the main thread and unstable list parameters causing recomposition, fixed image sizing with Coil, added keys and immutable models, and created a Baseline Profile. Result: frozen frames dropped by about 60 percent and startup improved by 25 percent in Macrobenchmark.

Frequently asked questions

Who should practise hard Android App Development questions?

Candidates targeting senior roles or top product companies, and anyone who finds the medium set easy.

How should I practise these?

Answer each question aloud before opening the answer, mark it as solved only when your answer matches the key idea, and save misses to Last-Minute Revision.

Other difficulty levels