Android App Development Interview Questions for 7 Years Experience
40 questions with answers for candidates with 7 years of experience — 1 easy, 19 medium, 20 hard.
What interviewers expect: 7 years experience
With seven years, roles are often lead or staff level. Interviews assess how you set technical direction: migrations like XML to Compose, adopting Kotlin Multiplatform, release strategy, and quality metrics across teams. Expect discussions about trade-offs with product and backend, cost of decisions and long-term maintainability. Your influence beyond your own code is the main thing being judged.
This set is weighted towards App Architecture, MVVM and Hilt (6), Networking and APIs (6), Jetpack Compose (6), Testing, Performance and Play Store (5).
Technical strategy and migrations Cross-team collaboration Quality and reliability metrics Hiring and growing engineers
Solved at this levelStart 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
How do you handle one-time navigation after a successful login in MVVM?
Recommended practice is to reflect the result in state, such as isLoggedIn = true in UiState, and have the UI react with LaunchedEffect(state.isLoggedIn) to navigate, then the ViewModel marks it consumed if needed. An alternative is a Channel exposed as Flow and collected with lifecycle awareness. Avoid calling navigation directly from the ViewModel, because it should not reference NavController or views.
02
Write an OkHttp interceptor that adds a bearer token to every request.
Analyse the build with APK Analyzer. Publish as an App Bundle so users get only their density, language and ABI. Enable R8 and resource shrinking, convert images to WebP or vectors, remove unused libraries and duplicate dependencies, and move large optional features or assets to Play Feature Delivery or Play Asset Delivery. Avoid bundling all native libraries for every ABI. Track size in CI.
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.
05
How do you design an Android client to support API versioning and forced updates?
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.
06
How do LazyColumn keys improve performance and correctness?
By default, items are identified by position, so inserting or deleting an item can make Compose reuse state for the wrong item and recompose more than needed. Supplying key = { it.id } gives each item a stable identity, so remembered state, animations with animateItem and scroll position follow the right item. Also supply contentType for mixed lists so compositions are reused efficiently. Keys must be unique and Bundle-saveable.
07
Your banking app must pass a security audit. What would you check on the Android side?
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.
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.
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.
10
How would you implement real-time order tracking updates in an app?
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.
11
How do you handle push notifications with Firebase Cloud Messaging?
Extend FirebaseMessagingService, override onNewToken to send the token to your server, and onMessageReceived to handle data messages. Notification messages are shown automatically by the system when the app is in background; data messages are always delivered to your code. Request POST_NOTIFICATIONS on Android 13 and above, create channels, and keep onMessageReceived short, using WorkManager for longer work triggered by the push.
12
Explain generics variance with in and out in Kotlin.
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.
13
The product team wants location tracking for delivery partners even when the app is closed. How do you implement it?
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.
On every pull request, run lint, ktlint or detekt, unit tests and a debug build in a CI service like GitHub Actions or Bitrise, with Gradle caching. On merge to main, build a signed release AAB using secrets, run instrumented tests on emulators or a device farm, and upload to the Play internal track with Gradle Play Publisher or fastlane. Promote to production manually after QA with a staged rollout.
When you change the schema, increase the database version and provide a Migration(from, to) with SQL like ALTER TABLE, or use AutoMigration for simple changes with exported schemas. Without a migration, Room throws at startup unless you call fallbackToDestructiveMigration, which wipes user data. Export schemas to version control and test migrations with MigrationTestHelper so upgrades from every old version are verified.
16
Explain LaunchedEffect, DisposableEffect and SideEffect.
LaunchedEffect(key) launches a coroutine when it enters composition and restarts when the key changes, used for one-time loads or showing a snackbar. DisposableEffect(key) runs setup and requires an onDispose block for cleanup, used for registering listeners or lifecycle observers. SideEffect runs after every successful recomposition to publish Compose state to non-Compose code, such as analytics. Choose keys carefully so effects do not restart unexpectedly.
17
Write a Room database class with a migration that adds a column.
Situation: after a release, crash-free users dropped from 99.6 to 97 percent. Task: stop the impact fast. Action: I checked Crashlytics, found a crash on Android 14 caused by a missing foreground service type, paused the staged rollout in Play Console, shipped a hotfix with a test on an emulator for that API level, and wrote a short postmortem. Result: crash rate recovered within a day and we added API-level checks to our release checklist.
19
A ViewModel has grown to 1500 lines handling many responsibilities. How would you refactor it?
First add tests around current behaviour so refactoring is safe. Then extract business logic into use cases or domain classes, move data mapping into repositories or mappers, and split UI state into smaller, focused data classes. If the screen truly has separate areas, consider separate state holders or child ViewModels per section. Refactor in small pull requests so reviewers can follow and regressions are easy to locate.
20
How would you lead an architecture decision when senior developers disagree?
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.
21
Explain backpressure handling in Flow with buffer, conflate and collectLatest.
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.
22
How do you migrate from SharedPreferences to DataStore without losing user data?
Use SharedPreferencesMigration when creating the DataStore: preferencesDataStore(name = "settings", produceMigrations = { ctx -> listOf(SharedPreferencesMigration(ctx, "old_prefs")) }). On first access, DataStore copies the values and deletes the old file. Update all reads and writes to go through the new repository at once, because mixing both stores leads to inconsistent values. Test the migration with existing preference files.
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.
24
Write a function that retries a suspend call with exponential backoff.
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.
27
What do flatMapLatest, flatMapConcat and flatMapMerge do?
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.
28
Write a manifest entry for a deep link that opens a product screen.
<activity
android:name=".MainActivity"
android:exported="true">
<intent-filter android:autoVerify="true">
<action android:name="android.intent.action.VIEW" />
<category android:name="android.intent.category.DEFAULT" />
<category android:name="android.intent.category.BROWSABLE" />
<data android:scheme="https"
android:host="shop.example.com"
android:pathPrefix="/product" />
</intent-filter>
</activity>
<!-- autoVerify plus assetlinks.json on the domain makes it an App Link -->
29
How do you structure a large Android app into modules?
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.
30
Explain how you would use Room with Kotlin Multiplatform.
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.
31
What is Jetpack Compose and how is it different from XML views?
Jetpack Compose is Android's modern declarative UI toolkit. You describe the UI as Kotlin functions of state, and Compose redraws the parts that change when state changes. With XML views you inflate layouts and imperatively update views with findViewById or binding. Compose removes adapters and much boilerplate, makes theming and previews easier, and interoperates with views through ComposeView and AndroidView, allowing gradual migration.
32
What does derivedStateOf do and when should you use it?
derivedStateOf creates state computed from other state, and only triggers recomposition when the computed result changes. A classic example is showing a scroll-to-top button: val showButton by remember { derivedStateOf { listState.firstVisibleItemIndex > 0 } }. Without it, every scroll pixel would recompose. Do not use it when the result changes as often as the input, because it only adds overhead.
33
The API team wants to switch to GraphQL. What changes on the Android side?
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.
34
How do you unit test a ViewModel that uses coroutines and StateFlow?
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.
35
What is the difference between Job and SupervisorJob?
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.
36
What is the difference between a deep link and an Android App Link?
A deep link is any intent filter that opens your app for a URI; if several apps can handle it, the user sees a chooser. An App Link is an HTTPS deep link verified with android:autoVerify and a Digital Asset Links file at /.well-known/assetlinks.json on your domain, containing your app's signing certificate fingerprint. Verified links open your app directly. Remember to include the Play App Signing key fingerprint.
37
What is the difference between @Provides and @Binds in Hilt?
@Provides is used in a module function that constructs and returns an object, needed for third-party classes like Retrofit or Room that you cannot annotate with @Inject. @Binds is used on an abstract function in an abstract module to tell Hilt which implementation to use for an interface, when the implementation already has an @Inject constructor. @Binds generates less code and is slightly more efficient.
38
What is a use case in the domain layer and when is it worth adding?
A use case is a class with one job, like GetDiscountedCartTotalUseCase, typically exposing operator fun invoke. It holds business logic that combines repositories or is reused by several ViewModels. It keeps ViewModels thin and logic testable. For simple apps where the ViewModel only calls one repository method, use cases add boilerplate without benefit, so the domain layer is optional in the official guidance.
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.
40
A crash report shows a NullPointerException in Kotlin code. How can that happen if Kotlin is null safe?
Common causes are the !! operator, platform types from Java code without nullability annotations, accessing a lateinit property before assignment, calling a function from a constructor before properties are initialised, and reflection-based libraries like Gson setting non-null fields to null. Fix it by removing !!, adding annotations or explicit nullable types at the Java boundary, and using a Kotlin-aware serializer such as kotlinx.serialization or Moshi with Kotlin codegen.
Frequently asked questions
What is asked in Android App Development interviews for 7 years of experience?
With seven years, roles are often lead or staff level. Interviews assess how you set technical direction: migrations like XML to Compose, adopting Kotlin Multiplatform, release strategy, and quality metrics across teams. Expect discussions about trade-offs with product and backend, cost of decisions and long-term maintainability. Your influence beyond your own code is the main thing being judged.
How many questions should I practise before a Android App Development interview?
Work through all 40 questions here, then the company-wise sets for the employers you are targeting. Quality matters more than count: be able to explain why each answer is right.
Which topics matter most for 7 years of experience?
Focus first on Technical strategy and migrations, Cross-team collaboration, Quality and reliability metrics, Hiring and growing engineers.