Skip to content

Android Developer Interview Questions & Answers

40 questions for Android Developer interviews — 2 easy, 32 medium, 6 hard.

About the Android Developer role

An Android developer owns features end to end, from UI and state management to networking, local storage and release. They use coroutines, Flow, Room, Retrofit and Hilt daily, write unit tests, and work closely with designers, backend engineers and QA. Most job openings in Indian product companies and service firms are for this role, typically requiring one to four years of experience.

Coroutines and Flow MVVM with Hilt Room and DataStore Retrofit and OkHttp Unit testing
Solved for this roleStart 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

Write a HiltViewModel that exposes a single UiState using StateFlow.

Save to ▾
Show answer
data class ProductUiState( val loading: Boolean = true, val items: List<Product> = emptyList(), val error: String? = null ) @HiltViewModel class ProductViewModel @Inject constructor( private val repo: ProductRepository ) : ViewModel() { private val _state = MutableStateFlow(ProductUiState()) val state: StateFlow<ProductUiState> = _state.asStateFlow() init { refresh() } fun refresh() = viewModelScope.launch { _state.update { it.copy(loading = true, error = null) } runCatching { repo.fetchProducts() } .onSuccess { list -> _state.update { it.copy(loading = false, items = list) } } .onFailure { _state.update { it.copy(loading = false, error = "Try again") } } } }
02

After an app update, users report that the app crashes on launch. Logs show a Room migration error. What do you do?

Save to ▾
Show answer
Immediately halt the staged rollout in Play Console. Reproduce by installing the previous version, adding data, and upgrading. The schema version was probably increased without a matching migration, or a migration SQL does not match the new entity. Write the correct Migration, test it with MigrationTestHelper from every supported old version, and release a hotfix. Avoid destructive fallback because it deletes user data.
03

Users on slow 2G and 3G networks in small towns report the app keeps failing. How do you improve it?

Save to ▾
Show answer
Set reasonable connect and read timeouts, retry idempotent requests with exponential backoff, and cache responses so screens show something offline. Reduce payload size with gzip, pagination and fields filtering, and load images in smaller sizes with placeholders. Show clear offline and retry states instead of generic errors. Test with the emulator network throttling and real devices on poor connections, and track failure rates by network type.
04

What is the difference between MVVM and MVI?

Save to ▾
Show answer
Both separate UI from logic. MVI (Model-View-Intent) is stricter: the UI sends intents or actions, a reducer produces a single new immutable state, and side effects are handled separately. It gives one state object per screen and an easy-to-replay history. MVVM is more flexible, sometimes exposing several streams. In practice many Compose apps use MVVM with a single UiState data class, which is close to MVI.
05

What is a use case in the domain layer and when is it worth adding?

Save to ▾
Show answer
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.
06

Where should API keys go in an Android app?

Save to ▾
Show answer
Anything shipped in the APK can be extracted, so treat client-side keys as public. Keep them out of version control using local.properties or CI secrets injected into BuildConfig, restrict them in the provider console by package name and SHA-1, and keep truly secret keys on your backend, which proxies the calls. Use Play Integrity or App Check to reduce abuse from modified apps.
07

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.
08

You need to cache API responses so the home screen opens instantly. What approach do you take?

Save to ▾
Show answer
Store the response in Room, or a file for a single blob, and have the screen observe the cached data immediately while a refresh runs in the background, known as stale-while-revalidate. Store a timestamp to decide when cache is too old. HTTP caching with OkHttp Cache can also help for GET requests with proper cache headers. Show cached content first, then update smoothly when fresh data arrives.
09

How do you mentor junior Android developers?

Save to ▾
Show answer
I pair on their first tasks, explain the why behind patterns rather than only giving fixes, and review pull requests with specific, kind comments plus links to docs. I give them small features end to end so they see the full flow from API to UI, and I ask them to present one learning in our weekly tech sync. I check in regularly so they feel safe asking questions early.
10

Tell me about a time you worked closely with a backend team to solve a problem.

Save to ▾
Show answer
Situation: our feed took four seconds to load because the app made six API calls. Task: reduce load time. Action: I measured each call with network profiling, shared a timeline with the backend team, and we agreed on one aggregated endpoint with only the fields the screen used, plus pagination. I updated the client and added caching. Result: load time dropped to under one second and both teams reused the approach.
11

Write a Retrofit interface for listing products with paging and fetching one product.

Save to ▾
Show answer
interface ShopApi { @GET("products") suspend fun getProducts( @Query("page") page: Int, @Query("size") size: Int = 20 ): ProductPageDto @GET("products/{id}") suspend fun getProduct(@Path("id") id: String): Response<ProductDto> @POST("cart") suspend fun addToCart(@Body body: AddToCartRequest): CartDto }
12

How do you test Retrofit networking code?

Save to ▾
Show answer
Use OkHttp MockWebServer to run a local server, enqueue responses with specific status codes, bodies and delays, and point Retrofit's base URL to it. Then call the API or repository and assert parsing, headers, error mapping and retry behaviour. Store sample JSON in test resources. For ViewModel tests, replace the repository with a fake instead of using MockWebServer, which keeps them fast.
13

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.
14

Explain StateFlow versus SharedFlow.

Save to ▾
Show answer
StateFlow is a hot flow that always has a current value, replays only the latest value to new collectors and skips emissions equal to the current value. It is ideal for UI state. SharedFlow is a configurable hot flow with no required initial value, optional replay and buffer, and it emits every value including duplicates. It suits one-off events like navigation or snackbars, though many teams prefer a Channel or state-based events for those.
15

How do you write tests for a Room DAO?

Save to ▾
Show answer
Create an in-memory database with Room.inMemoryDatabaseBuilder in an instrumented test or with Robolectric, get the DAO, insert test data and assert query results. For Flow queries, collect the first value with first() or use Turbine. Close the database in @After. Test edge cases like conflict strategies and ordering. In-memory databases are fast and isolated, so each test starts clean.
16

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.
17

What is SavedStateHandle and when do you use it?

Save to ▾
Show answer
SavedStateHandle is a key-value map available to a ViewModel that is saved into the instance state Bundle, so it survives process death as well as configuration changes. Navigation arguments are also placed in it automatically. Use it for small, essential UI state like a search query, selected filter or the ID of the item being edited. Do not put large lists in it; reload those from the repository.
18

Network calls keep running after the user leaves a screen and results arrive late. How do you fix this?

Save to ▾
Show answer
The work is likely launched in GlobalScope, a custom scope never cancelled, or an Application-level scope. Move it to viewModelScope so it is cancelled when the ViewModel is cleared, and make sure the calls are cancellable, which Retrofit suspend functions are. If work must outlive the screen, like uploading a file, move it to WorkManager or an application scope deliberately, and do not update UI from it.
19

How do you store relationships in Room, such as orders with items?

Save to ▾
Show answer
Create separate entities and link them with a foreign key column, like OrderItem.orderId. Then define a data class with @Embedded order and @Relation(parentColumn = "id", entityColumn = "orderId") items, and query it in a DAO method annotated with @Transaction so both reads are consistent. For many-to-many relations, add a cross-reference entity with junction. Add indices on foreign key columns for query speed.
20

Write a Room entity and DAO for notes with a Flow query.

Save to ▾
Show answer
@Entity(tableName = "notes") data class NoteEntity( @PrimaryKey(autoGenerate = true) val id: Long = 0, val title: String, val body: String, val updatedAt: Long ) @Dao interface NoteDao { @Query("SELECT * FROM notes ORDER BY updatedAt DESC") fun observeAll(): Flow<List<NoteEntity>> @Upsert suspend fun upsert(note: NoteEntity) @Query("DELETE FROM notes WHERE id = :id") suspend fun delete(id: Long) }
21

Tell me about a time you disagreed with a designer about a UI decision.

Save to ▾
Show answer
Situation: a designer wanted a custom tiny close icon on a full-screen dialog. Task: keep the app accessible without blocking the design. Action: I showed that the touch target was below the 48dp guideline and that TalkBack had no label, and prototyped the same look with a larger invisible touch area. Result: we kept the visual style, passed accessibility checks, and agreed to review touch targets in future design handoffs.
22

The app shows an ANR when the user taps Save. The save function is suspend. What could be wrong?

Save to ▾
Show answer
A suspend function does not automatically run off the main thread. If it calls blocking code, like a synchronous file write, a SharedPreferences commit, a blocking SDK or heavy JSON parsing, it still blocks the main thread. Check with a profiler or StrictMode. Fix by wrapping the blocking part in withContext(Dispatchers.IO) or Default, or by using main-safe APIs such as Room suspend DAOs or DataStore.
23

Write a Flow that combines user settings and product list into one UI state.

Save to ▾
Show answer
val uiState: StateFlow<HomeState> = combine( settingsRepo.settings, productRepo.products ) { settings, products -> HomeState( currency = settings.currency, items = products.sortedBy { it.price } ) }.stateIn( scope = viewModelScope, started = SharingStarted.WhileSubscribed(5000), initialValue = HomeState() )
24

How do you share data between two screens in a checkout flow?

Save to ▾
Show answer
Options in order of preference: pass small IDs as navigation arguments and load the rest from a repository; store flow data in a repository scoped to the session; or use a ViewModel scoped to a nested navigation graph so all checkout screens share it and it is cleared when the flow ends. Avoid activity-scoped ViewModels holding everything, and avoid passing large objects through arguments.
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

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.
27

How do you handle one-time navigation after a successful login in MVVM?

Save to ▾
Show answer
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.
28

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.
29

Write a Room database class with a migration that adds a column.

Save to ▾
Show answer
@Database(entities = [NoteEntity::class], version = 2, exportSchema = true) abstract class AppDatabase : RoomDatabase() { abstract fun noteDao(): NoteDao } val MIGRATION_1_2 = object : Migration(1, 2) { override fun migrate(db: SupportSQLiteDatabase) { db.execSQL("ALTER TABLE notes ADD COLUMN pinned INTEGER NOT NULL DEFAULT 0") } } val db = Room.databaseBuilder(context, AppDatabase::class.java, "app.db") .addMigrations(MIGRATION_1_2) .build()
30

A screen shows a snackbar again every time the device rotates. How do you fix it?

Save to ▾
Show answer
The event is probably stored as state in a StateFlow or LiveData, so it is re-delivered when the new Activity collects again. Options: model it as part of UI state and have the UI call a function like onMessageShown() to clear it after display, which is the approach the official guidance recommends; or send it through a Channel exposed with receiveAsFlow so each event is consumed once. Collect with lifecycle awareness to avoid losing events.
31

How do you find and fix memory leaks in an Android app?

Save to ▾
Show answer
Add LeakCanary to debug builds; it detects retained Activities, Fragments and ViewModels and shows the reference chain. Common leaks are static or singleton references to Context or Views, listeners not unregistered, inner classes holding Activity references, and Fragment view bindings not cleared in onDestroyView. Fix by using application context, lifecycle-aware components and weak references only where needed. Confirm with Memory Profiler heap dumps.
32

How do you migrate from SharedPreferences to DataStore without losing user data?

Save to ▾
Show answer
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.
33

How do Room migrations work?

Save to ▾
Show answer
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.
34

Write a use case that combines cart items and a coupon into a final total.

Save to ▾
Show answer
class GetCartTotalUseCase @Inject constructor( private val cartRepo: CartRepository, private val couponRepo: CouponRepository ) { operator fun invoke(): Flow<Double> = combine(cartRepo.items, couponRepo.activeCoupon) { items, coupon -> val subtotal = items.sumOf { it.price * it.qty } val discount = coupon?.let { minOf(subtotal * it.percent / 100, it.maxOff) } ?: 0.0 subtotal - discount } }
35

You are starting a new app for a fintech startup. Which architecture and libraries would you choose and why?

Save to ▾
Show answer
I would use Kotlin, Jetpack Compose with Material 3, MVVM with unidirectional data flow, Hilt for DI, Coroutines and Flow, Retrofit with OkHttp and kotlinx.serialization, Room for offline data, DataStore for preferences, WorkManager for background sync, and Navigation Compose. For fintech, add certificate pinning considerations, encrypted storage via Keystore, Play Integrity checks, and strong test coverage. Start modular from day one with core and feature modules.
36

What is targetSdkVersion and why does Play enforce updates?

Save to ▾
Show answer
targetSdkVersion tells Android which platform behaviour your app was tested against; the system applies newer behaviour changes only when you target that level. The Play Store requires new apps and updates to target a recent API level, usually within one year of a release, so users benefit from security and privacy changes. minSdk sets the oldest supported version and compileSdk the API you compile against.
37

Write an OkHttp interceptor that adds a bearer token to every request.

Save to ▾
Show answer
class AuthInterceptor @Inject constructor( private val tokenStore: TokenStore ) : Interceptor { override fun intercept(chain: Interceptor.Chain): Response { val token = tokenStore.currentAccessToken() val request = chain.request().newBuilder().apply { if (token != null) header("Authorization", "Bearer $token") }.build() return chain.proceed(request) } } val client = OkHttpClient.Builder() .addInterceptor(authInterceptor) .build()
38

How should a Fragment or Activity collect a Flow safely?

Save to ▾
Show answer
Use lifecycleScope.launch with repeatOnLifecycle(Lifecycle.State.STARTED) so collection starts when the UI is visible and stops in the background, avoiding wasted work and crashes from updating views that are not ready. In a Fragment, use viewLifecycleOwner.lifecycleScope because the fragment view can be destroyed while the fragment lives. In Compose, use collectAsStateWithLifecycle(), which does the same lifecycle-aware collection.
39

Users who reinstall the app lose all their settings. How can you restore them?

Save to ▾
Show answer
Android Auto Backup automatically backs up app data to the user's cloud backup account for apps targeting API 23 and above, unless disabled. Configure dataExtractionRules for Android 12 and above to include settings files and exclude tokens or caches. For important data like notes, sync to your own server tied to the user's account, since backup is not guaranteed. Test with adb shell bmgr commands.
40

Compare kotlinx.serialization, Moshi and Gson for JSON parsing.

Save to ▾
Show answer
kotlinx.serialization is Kotlin-first, uses a compiler plugin with no reflection, respects default values and nullability, and works in Kotlin Multiplatform. Moshi supports Kotlin through codegen and is also reliable. Gson uses reflection, ignores Kotlin nullability and default values, and can put null into non-null fields, causing crashes later; it is in maintenance mode. For new Android projects, kotlinx.serialization is the common choice.

Frequently asked questions

What does a Android Developer do?

An Android developer owns features end to end, from UI and state management to networking, local storage and release. They use coroutines, Flow, Room, Retrofit and Hilt daily, write unit tests, and work closely with designers, backend engineers and QA. Most job openings in Indian product companies and service firms are for this role, typically requiring one to four years of experience.

Which skills are tested in a Android Developer interview?

Expect questions on Coroutines and Flow, MVVM with Hilt, Room and DataStore, Retrofit and OkHttp, Unit testing. Most rounds also ask you to explain a project and solve one practical problem live.

How do I prepare for a Android Developer interview?

Revise Coroutines and Flow, App Architecture, MVVM and Hilt, Networking and APIs, Room, DataStore and WorkManager first using the topic sheets, practise the questions on this page aloud, then do one full mock interview to practise timing.