Skip to content

Android App Development Interview Questions & Answers

All 200 unique questions in one place — 60 easy, 99 medium, 41 hard — grouped by topic.

Practise by topic

Solved overallStart 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 is the difference between val and var in Kotlin?

Save to ▾
Show answer
val declares a read-only reference that cannot be reassigned after initialisation, while var declares a mutable reference. Note that val does not make the object immutable: a val pointing to a MutableList can still have items added. Prefer val by default because it makes code easier to reason about and safer in concurrent code, and switch to var only when reassignment is genuinely needed.
02

How does Kotlin handle null safety?

Save to ▾
Show answer
Kotlin's type system separates nullable types (String?) from non-nullable types (String). The compiler stops you calling methods on a nullable value without a check. You use the safe call operator ?., the Elvis operator ?: for defaults, smart casts after null checks, or !! to force a value and risk a NullPointerException. This moves most null crashes from runtime to compile time, which is a major reason Android adopted Kotlin.
03

What is a data class and what does the compiler generate for it?

Save to ▾
Show answer
A data class is a class meant to hold data, declared with the data keyword and at least one property in the primary constructor. The compiler generates equals(), hashCode(), toString(), copy() and componentN() functions for destructuring, all based on primary constructor properties. They are ideal for UI state, API models and Room entities. Properties declared in the class body are not included in the generated equals or toString.
04

Write a sealed interface for a screen state and handle it with when.

Save to ▾
Show answer
sealed interface UiState { data object Loading : UiState data class Success(val items: List<String>) : UiState data class Error(val message: String) : UiState } fun render(state: UiState) = when (state) { UiState.Loading -> showProgress() is UiState.Success -> showList(state.items) is UiState.Error -> showError(state.message) }
05

What is the difference between == and === in Kotlin?

Save to ▾
Show answer
== checks structural equality and calls equals() under the hood, handling nulls safely. === checks referential equality, meaning both references point to the same object in memory. For two data class instances with the same values, == returns true but === returns false unless they are the same instance. For primitive-like types such as Int, === may behave unexpectedly due to boxing, so == is the normal choice.
06

What are higher-order functions and lambdas in Kotlin?

Save to ▾
Show answer
A higher-order function takes a function as a parameter or returns one. A lambda is an anonymous function literal like { x: Int -> x * 2 }. Collection operations such as map, filter and forEach are higher-order functions. In Android you see them in click listeners, Compose callbacks like onClick = { } and coroutine builders. If the last parameter is a function, the lambda can be written outside the parentheses, known as trailing lambda syntax.
07

Write a Kotlin function that groups a list of orders by city and returns total amount per city.

Save to ▾
Show answer
data class Order(val city: String, val amount: Double) fun totalsByCity(orders: List<Order>): Map<String, Double> = orders.groupBy { it.city } .mapValues { (_, list) -> list.sumOf { it.amount } } // Alternative with one pass: // orders.groupingBy { it.city }.fold(0.0) { acc, o -> acc + o.amount }
08

How do default and named arguments reduce the need for overloads?

Save to ▾
Show answer
Kotlin functions can define default values, such as fun showToast(msg: String, long: Boolean = false), so callers skip arguments they do not need. Named arguments, like showToast(msg = "Saved", long = true), make calls readable and let you change argument order. Together they replace telescoping constructors and builder patterns in many cases. For Java callers, add @JvmOverloads so overloads are generated automatically.
09

A teammate uses !! everywhere to silence compiler errors. How do you handle this in code review?

Save to ▾
Show answer
Explain calmly that !! converts a compile-time check into a possible runtime crash. Suggest alternatives per case: safe calls with ?.let, Elvis with a default or early return, requireNotNull with a clear message, or fixing the type at the source so it is non-null. Add a detekt rule to flag !! in new code. Pair with them on one file so the change feels like help, not criticism.
10

Write a function that safely parses a string to an Int and returns a default on failure.

Save to ▾
Show answer
fun String?.toIntOrDefault(default: Int = 0): Int = this?.trim()?.toIntOrNull() ?: default // Usage val qty = input.text.toString().toIntOrDefault(1) val age = null.toIntOrDefault() // 0
11

Tell me about a time you learned a new language or framework quickly for a project.

Save to ▾
Show answer
Situation: my team switched a feature to Kotlin when I only knew Java. Task: deliver a profile screen in two weeks. Action: I spent the first evenings on the official Kotlin koans, converted small utility classes first, and asked a senior to review my early pull requests for idiomatic usage. Result: the screen shipped on time, and my later code needed far fewer review comments. I now learn by building something small immediately.
12

Why should we hire you as a fresher Android developer?

Save to ▾
Show answer
Give evidence, not adjectives. For example: I have built and published two apps using Kotlin and Jetpack Compose, one with Room and Retrofit and an MVVM structure, and both are on my GitHub with a README and tests. I debug by reading logs and stack traces before asking for help, and I am comfortable learning from code reviews. I can contribute to bug fixes and small features from the first month.
13

Explain the difference between let, run, with, apply and also.

Save to ▾
Show answer
All are scope functions that run a block on an object. let and also receive the object as it; run, with and apply receive it as this. let, run and with return the lambda result, while apply and also return the object itself. Typical use: let for null checks (user?.let { }), apply for configuring an object, also for side effects like logging, and run or with for computing a result from an object.
14

What is a sealed class and when would you use it on Android?

Save to ▾
Show answer
A sealed class restricts its subclasses to a known set defined in the same package and module. The compiler knows every subtype, so a when expression over it can be exhaustive without an else branch. On Android it is commonly used to model UI state such as Loading, Success(data) and Error(message), or one-time events. Adding a new subtype causes compile errors wherever handling is missing, which prevents forgotten cases.
15

What are extension functions and how are they resolved?

Save to ▾
Show answer
Extension functions let you add functions to an existing class without inheriting from it, for example fun String.isValidEmail(): Boolean. They are resolved statically at compile time based on the declared type of the variable, not the runtime type, so they do not support polymorphism. They cannot access private members of the class. They are used widely in Android KTX libraries, such as Context.toast or View.isVisible.
16

What is the difference between lateinit and by lazy?

Save to ▾
Show answer
lateinit works only with var and non-primitive, non-null types; you promise to assign it before use, and accessing it early throws UninitializedPropertyAccessException. by lazy works with val; the initialiser runs once on first access and the result is cached, thread-safe by default. Use lateinit for values injected or set in lifecycle callbacks, such as a binding in onCreate, and lazy for expensive values computed on demand.
17

Explain object, companion object and object expressions.

Save to ▾
Show answer
An object declaration creates a thread-safe singleton initialised lazily on first access. A companion object is an object tied to a class, used for factory methods and constants, similar to static members in Java; add @JvmStatic for Java callers. An object expression creates an anonymous object on the spot, often to implement an interface such as a callback. Avoid holding Context or Activity references in singletons to prevent memory leaks.
18

What is the difference between List and MutableList, and is List truly immutable?

Save to ▾
Show answer
List is a read-only interface exposing only reading operations, while MutableList adds add, remove and set. A List is not guaranteed immutable: the underlying object might be an ArrayList that another reference can still modify. For real immutability use kotlinx.collections.immutable or create defensive copies. In ViewModels, expose List or StateFlow<List> publicly and keep the mutable version private to protect state.
19

What are sequences in Kotlin and when are they better than collections?

Save to ▾
Show answer
Sequences evaluate lazily, element by element, while collection operations like map and filter create a new intermediate list at every step. For long chains on large data, or when you only need the first few results with take or first, asSequence() avoids extra allocations. For small lists, plain collections are often faster because sequences add per-element overhead. Measure before switching in performance-critical code.
20

Your team is migrating a Java Android app to Kotlin. How would you plan it?

Save to ▾
Show answer
Kotlin and Java interoperate, so migrate gradually rather than all at once. Start with new features in Kotlin, then convert leaf classes like models and utilities, writing tests before converting. Add nullability annotations to Java code so Kotlin sees correct types. Review auto-converted code because it often overuses !!. Track progress per module and agree on style rules with ktlint or detekt so the codebase stays consistent.
21

A crash report shows a NullPointerException in Kotlin code. How can that happen if Kotlin is null safe?

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

Write a delegated property that logs whenever a value changes.

Save to ▾
Show answer
import kotlin.properties.Delegates class Settings { var theme: String by Delegates.observable("light") { prop, old, new -> println("${prop.name} changed from $old to $new") } } // Use Delegates.vetoable if you need to reject a change // by returning false from the lambda.
23

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

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

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

What is a coroutine and how is it different from a thread?

Save to ▾
Show answer
A coroutine is a suspendable computation that can pause at suspension points without blocking the thread it runs on. Thousands of coroutines can share a small thread pool because suspended coroutines do not hold a thread. Threads are managed by the OS, are heavier in memory and context switching. On Android, coroutines let you write asynchronous code like network calls in a sequential style, without callbacks, while keeping the main thread free.
27

What does the suspend keyword mean?

Save to ▾
Show answer
suspend marks a function that can pause and resume later without blocking the thread. It can only be called from another suspend function or a coroutine. The compiler transforms it using continuation-passing style into a state machine. suspend alone does not move work to a background thread; the function must use withContext(Dispatchers.IO) or a main-safe library like Retrofit or Room to avoid blocking the main thread.
28

Explain Dispatchers.Main, IO and Default.

Save to ▾
Show answer
Dispatchers.Main runs on the Android UI thread and is used for updating views or state. Dispatchers.IO uses a large shared pool, by default up to 64 threads, for blocking work like disk and network. Dispatchers.Default uses a pool sized to CPU cores for CPU-heavy work like sorting, parsing large JSON or image processing. Use withContext to switch, and keep suspend functions main-safe so callers do not need to know.
29

What is the difference between launch and async?

Save to ▾
Show answer
launch starts a coroutine for work that returns no result and gives back a Job you can cancel or join. async returns a Deferred<T>, and you call await() to get the result. Use async to run independent tasks in parallel, such as fetching profile and orders together, then awaiting both. Exceptions differ: launch propagates them to the parent immediately, while async holds them until await, though in structured concurrency the parent still gets cancelled.
30

What is Kotlin Flow and how is it different from a suspend function?

Save to ▾
Show answer
A suspend function returns a single value once. A Flow is a cold asynchronous stream that emits multiple values over time, like database updates, location changes or search results. Cold means the code inside flow { } runs only when collected, and runs again for each collector. Room, DataStore and many libraries expose Flow so the UI automatically updates when underlying data changes.
31

How do you keep up with changes in Kotlin and Android?

Save to ▾
Show answer
I follow the Android Developers blog and release notes for Jetpack libraries, read the Kotlin release notes for each version, and watch sessions from the main yearly Android conferences. I try new APIs in a small side project before proposing them at work, and I read pull requests from experienced colleagues. Once a month I review deprecation warnings in our project so upgrades stay small and planned.
32

What is structured concurrency?

Save to ▾
Show answer
Structured concurrency means every coroutine runs inside a scope, and the scope does not complete until all its children complete. If the scope is cancelled, all children are cancelled; if a child fails, the failure propagates and cancels siblings and the parent by default. This prevents leaked background work. On Android, viewModelScope and lifecycleScope tie coroutines to component lifecycles so work stops when the screen goes away.
33

What is the difference between viewModelScope and lifecycleScope?

Save to ▾
Show answer
viewModelScope is cancelled when the ViewModel is cleared, so it survives configuration changes like rotation and suits loading data. lifecycleScope is tied to an Activity or Fragment lifecycle and is cancelled when it is destroyed, so it suits UI-related work. For collecting flows in views, combine lifecycleScope with repeatOnLifecycle(Lifecycle.State.STARTED) so collection stops when the app goes to background. Avoid GlobalScope in app code.
34

Write code to fetch two API results in parallel inside a ViewModel.

Save to ▾
Show answer
fun loadDashboard() { viewModelScope.launch { _state.value = UiState.Loading try { val (profile, orders) = coroutineScope { val p = async { repo.getProfile() } val o = async { repo.getOrders() } p.await() to o.await() } _state.value = UiState.Success(profile, orders) } catch (e: IOException) { _state.value = UiState.Error("Network error") } } }
35

How do you handle exceptions in coroutines?

Save to ▾
Show answer
Use try-catch around suspend calls for expected failures like network errors; this is the clearest approach. A CoroutineExceptionHandler on a root launch catches uncaught exceptions as a last resort for logging, but it does not work for async or child coroutines. For async, exceptions surface at await. Always rethrow CancellationException. Many teams wrap results in a Result or sealed type so the UI layer gets an explicit error state.
36

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

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

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() )
39

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

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

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

Describe a time you fixed a difficult concurrency bug.

Save to ▾
Show answer
Situation: our cart total sometimes showed wrong values after quick taps. Task: find the race. Action: I added logs with thread names and found two coroutines updating the same mutable list. I changed state updates to MutableStateFlow.update {}, which is atomic, and added a test with many concurrent updates. Result: the bug disappeared, and I shared a short note with the team on safe state updates so it would not repeat.
43

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

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

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 }
46

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

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 { }.
48

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

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

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

What are the four main Android app components?

Save to ▾
Show answer
Activities provide a screen the user interacts with. Services run work without a UI, such as music playback. Broadcast receivers respond to system or app-wide events like boot completed or connectivity change. Content providers share structured data between apps, such as contacts. Each component is an entry point the system can start, and most must be declared in AndroidManifest.xml with appropriate intent filters and permissions.
52

Explain the Activity lifecycle.

Save to ▾
Show answer
onCreate runs once to set up the UI and state. onStart makes the activity visible, onResume makes it interactive in the foreground. onPause runs when it loses focus, onStop when it is no longer visible, and onDestroy before it is removed. Rotation by default destroys and recreates the activity. Release resources like camera in onPause or onStop, and save transient UI state with ViewModel and SavedStateHandle.
53

What happens on a configuration change such as screen rotation?

Save to ▾
Show answer
By default the system destroys and recreates the Activity so it can load resources for the new configuration, like landscape layouts or a new language. Fields in the Activity are lost, but a ViewModel survives because it is kept by the ViewModelStore. Views with IDs restore some state automatically, and rememberSaveable in Compose restores small state. Handling changes manually via configChanges is possible but should be a deliberate choice.
54

What is the difference between explicit and implicit intents?

Save to ▾
Show answer
An explicit intent names the exact component to start, like Intent(this, DetailActivity::class.java), and is used inside your own app. An implicit intent declares an action and data, such as ACTION_VIEW with a URL or ACTION_SEND with text, and the system finds apps that can handle it through intent filters. For implicit intents, handle the case where no app can respond, and since Android 11 declare package visibility queries when needed.
55

How do runtime permissions work in modern Android?

Save to ▾
Show answer
Dangerous permissions like camera, location and notifications (POST_NOTIFICATIONS since Android 13) must be declared in the manifest and requested at runtime. Use the Activity Result API with RequestPermission or RequestMultiplePermissions contracts. Show a rationale when shouldShowRequestPermissionRationale returns true, handle denial gracefully, and direct users to settings if they deny permanently. Request permissions in context, at the moment the feature needs them.
56

What is the role of AndroidManifest.xml?

Save to ▾
Show answer
The manifest declares the app's package identity, components (activities, services, receivers, providers), intent filters, permissions, hardware features, and app-level settings like theme, backup rules and exported flags. Since Android 12, components with intent filters must set android:exported explicitly. Gradle merges manifests from libraries into the final one, so check the merged manifest when unexpected permissions appear in your release build.
57

What is the difference between Parcelable and Serializable?

Save to ▾
Show answer
Serializable is a Java marker interface that uses reflection and creates many temporary objects, so it is slower. Parcelable is Android-specific and writes fields explicitly to a Parcel, which is much faster for IPC. Writing Parcelable by hand is verbose, but the kotlin-parcelize plugin with @Parcelize generates it. Use Parcelable for Bundles and intents, and never use either as a long-term storage format.
58

How do you explain a technical limitation to a non-technical product manager?

Save to ▾
Show answer
I focus on user impact and options rather than jargon. For example, when asked for background tracking every second, I explained that Android limits background work to save battery, so the app would need a visible notification and stricter Play review. I offered two options with trade-offs and a recommendation. Using a simple diagram helped, and the PM chose the option that met the business need with fewer risks.
59

Explain the Fragment lifecycle and why viewLifecycleOwner matters.

Save to ▾
Show answer
A Fragment has its own lifecycle (onCreate to onDestroy) and a separate view lifecycle (onCreateView to onDestroyView). When a fragment goes to the back stack its view is destroyed but the fragment instance may stay. If you observe LiveData or collect flows with the fragment itself as owner, observers pile up and can touch a dead view. Use viewLifecycleOwner and clear view binding references in onDestroyView.
60

What are launch modes and task affinity?

Save to ▾
Show answer
Launch modes control how an Activity instance joins a task. standard creates a new instance each time. singleTop reuses the instance at the top and calls onNewIntent. singleTask keeps one instance in its task and clears activities above it. singleInstance puts it alone in a task, and singleInstancePerTask exists since Android 12. Intent flags like FLAG_ACTIVITY_CLEAR_TOP give similar control. Most modern single-activity apps rarely need them.
61

What is the difference between a started service, bound service and foreground service?

Save to ▾
Show answer
A started service runs after startService until it stops itself. A bound service offers a client-server interface to components that bind to it and stops when all unbind. A foreground service shows a persistent notification and has higher priority, used for music, navigation or ongoing calls. Since Android 14 a foreground service must declare a type such as mediaPlayback or location. For deferrable background work, use WorkManager instead.
62

Write code to request camera permission using the Activity Result API.

Save to ▾
Show answer
private val cameraPermission = registerForActivityResult( ActivityResultContracts.RequestPermission() ) { granted -> if (granted) openCamera() else showPermissionDenied() } fun onScanClicked() { when { ContextCompat.checkSelfPermission(this, Manifest.permission.CAMERA) == PackageManager.PERMISSION_GRANTED -> openCamera() shouldShowRequestPermissionRationale(Manifest.permission.CAMERA) -> showRationale { cameraPermission.launch(Manifest.permission.CAMERA) } else -> cameraPermission.launch(Manifest.permission.CAMERA) } }
63

What is a BroadcastReceiver and what limits apply since Android 8?

Save to ▾
Show answer
A BroadcastReceiver responds to broadcast intents. Since Android 8, most implicit broadcasts cannot be received by receivers declared in the manifest; you register them at runtime with registerReceiver while the app is running. A few exemptions exist, like BOOT_COMPLETED and locale change. Since Android 14, runtime receivers must specify RECEIVER_EXPORTED or RECEIVER_NOT_EXPORTED. onReceive runs on the main thread, so keep it short or use goAsync.
64

What is a ContentProvider and when would you build one?

Save to ▾
Show answer
A ContentProvider exposes data through a URI-based interface with query, insert, update and delete, and handles permissions for cross-app access. You need one when sharing data with other apps, providing search suggestions, or supporting widgets that read your data. FileProvider is a ready-made provider for sharing files securely with content URIs instead of file paths. For data used only inside your app, Room is simpler.
65

How do you pass data between screens and what is the size limit?

Save to ▾
Show answer
Traditionally through Intent extras or Fragment arguments as a Bundle with primitives, Parcelable or Serializable objects. Bundles go through Binder transactions, which have a shared limit of about 1 MB, so large data causes TransactionTooLargeException. Pass an ID instead and load the data from a repository or database on the next screen. With Navigation, use type-safe route arguments and a shared ViewModel only when screens truly share state.
66

Write a manifest entry for a deep link that opens a product screen.

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

What is the difference between a deep link and an Android App Link?

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

Users report the app loses form data when they switch to another app and come back after a while. Why?

Save to ▾
Show answer
The system killed the process to reclaim memory while the app was in the background. When the user returns, Android recreates the Activity but ViewModel data is gone because the process died. Fix it by storing small UI state in SavedStateHandle or rememberSaveable, which survive process death, and persisting drafts to Room or DataStore. Test with Don't keep activities or adb shell am kill.
69

Write code to show a notification with a channel.

Save to ▾
Show answer
fun showOrderNotification(context: Context, text: String) { val channelId = "orders" if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { val channel = NotificationChannel( channelId, "Order updates", NotificationManager.IMPORTANCE_DEFAULT) context.getSystemService(NotificationManager::class.java) .createNotificationChannel(channel) } val n = NotificationCompat.Builder(context, channelId) .setSmallIcon(R.drawable.ic_order) .setContentTitle("Order update") .setContentText(text) .setAutoCancel(true) .build() NotificationManagerCompat.from(context).notify(1001, n) }
70

How do you build a single-activity app with the Navigation component?

Save to ▾
Show answer
Keep one Activity hosting a NavHost, and represent each screen as a Compose destination or Fragment. Define routes in a navigation graph, preferably with type-safe routes using Kotlin serialization in Navigation 2.8 and later. Use nested graphs for flows like login or checkout, pop the back stack with popUpTo when finishing a flow, and handle deep links on destinations. This simplifies shared transitions and state handling.
71

Tell me about a time you handled a production crash under pressure.

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

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

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

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

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

What is Jetpack Compose and how is it different from XML views?

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

What is recomposition?

Save to ▾
Show answer
Recomposition is Compose re-running composable functions whose inputs or read state changed, to update the UI. Compose tries to skip composables whose parameters are stable and unchanged. Composables can run often and in any order, so they must be side-effect free and fast; do not make network calls or write to variables inside them. Use Layout Inspector recomposition counts to find composables recomposing more than expected.
78

What is the difference between remember and rememberSaveable?

Save to ▾
Show answer
remember keeps a value across recompositions but loses it when the composable leaves composition, on configuration change or process death. rememberSaveable stores the value in the saved instance state Bundle, so it survives rotation and process death; it works with types that can go in a Bundle or with a custom Saver. Use rememberSaveable for things like text input or selected tab, and a ViewModel for screen-level data.
79

What is state hoisting in Compose?

Save to ▾
Show answer
State hoisting means moving state out of a composable to its caller, and passing the value plus event callbacks down, like value and onValueChange. The composable becomes stateless, reusable and easy to preview and test. State should be hoisted to the lowest common parent that needs to read or change it, and screen-level state usually lives in a ViewModel. This creates unidirectional data flow: state flows down, events flow up.
80

Write a stateless text field with a hoisted state and validation message.

Save to ▾
Show answer
@Composable fun EmailField( value: String, onValueChange: (String) -> Unit, error: String?, modifier: Modifier = Modifier ) { OutlinedTextField( value = value, onValueChange = onValueChange, label = { Text("Email") }, isError = error != null, supportingText = { error?.let { Text(it) } }, singleLine = true, modifier = modifier.fillMaxWidth() ) }
81

Write a LazyColumn that shows a list of products with keys and a click handler.

Save to ▾
Show answer
@Composable fun ProductList( products: List<Product>, onClick: (Product) -> Unit ) { LazyColumn( contentPadding = PaddingValues(16.dp), verticalArrangement = Arrangement.spacedBy(8.dp) ) { items(products, key = { it.id }) { product -> ListItem( headlineContent = { Text(product.name) }, supportingContent = { Text("Rs ${product.price}") }, modifier = Modifier.clickable { onClick(product) } ) } } }
82

Why does the order of modifiers matter?

Save to ▾
Show answer
Modifiers are applied in a chain from left to right, each wrapping the next. Modifier.padding(16.dp).background(Color.Red) adds padding outside the red area, while background first then padding colours the padding too. Similarly, clickable before padding makes the whole padded area clickable. Thinking of each modifier as wrapping the content after it helps predict layout, size and touch target behaviour.
83

How do you collect a ViewModel StateFlow in a composable?

Save to ▾
Show answer
Use val state by viewModel.uiState.collectAsStateWithLifecycle() from lifecycle-runtime-compose. It collects only while the lifecycle is at least STARTED, saving resources in the background, unlike collectAsState which keeps collecting. Pass the resulting state and callbacks to a stateless screen composable, which keeps the screen previewable and testable without a ViewModel. Obtain the ViewModel with hiltViewModel() or viewModel() at the route level.
84

A composable makes an API call every time the screen recomposes. What is wrong and how do you fix it?

Save to ▾
Show answer
The call is placed directly in the composable body, which runs on every recomposition. Composables must be side-effect free. Move loading to the ViewModel, triggered in init or by a user event, and expose the result as StateFlow. If something must start from composition, wrap it in LaunchedEffect with a stable key like the item ID, so it runs once per key rather than on every recomposition.
85

A text field loses its typed value after rotation. How do you fix it?

Save to ▾
Show answer
The value is probably held with remember, which is lost when the Activity is recreated. Either use rememberSaveable for local UI state, or keep the text in the ViewModel and pass it down with onValueChange. For data that must survive process death too, store it in SavedStateHandle in the ViewModel. Avoid storing text field state asynchronously, because delayed updates can cause cursor jumps.
86

Explain LaunchedEffect, DisposableEffect and SideEffect.

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

What does derivedStateOf do and when should you use it?

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

How do LazyColumn keys improve performance and correctness?

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

What is CompositionLocal and when should you use it?

Save to ▾
Show answer
CompositionLocal passes data implicitly down the tree without threading it through every parameter, like LocalContext, LocalDensity or MaterialTheme values. Create one with compositionLocalOf when values change often, or staticCompositionLocalOf when they rarely change. Use it for cross-cutting values like theme or analytics loggers, not for regular screen data, because implicit dependencies make composables harder to understand and test.
90

How do you implement theming with Material 3 in Compose?

Save to ▾
Show answer
Wrap the app in a MaterialTheme with a ColorScheme, Typography and Shapes. Use lightColorScheme and darkColorScheme, pick based on isSystemInDarkTheme(), and use dynamicLightColorScheme or dynamicDarkColorScheme on Android 12 and above for Material You colours. Components read MaterialTheme.colorScheme.primary and similar roles, so avoid hardcoding colours. Extend the theme with a custom CompositionLocal for brand-specific tokens.
91

How do you test a Compose UI?

Save to ▾
Show answer
Use the Compose testing APIs with createComposeRule or createAndroidComposeRule. Find nodes by text, content description or testTag, then perform actions and assertions, like onNodeWithText("Login").performClick() and onNodeWithTag("error").assertIsDisplayed(). Test stateless composables with fake state for speed. Tests synchronise with Compose automatically. Screenshot testing tools like Roborazzi or Paparazzi catch visual regressions without a device.
92

Write a Compose UI test that checks an error appears when login is clicked with an empty email.

Save to ▾
Show answer
@get:Rule val rule = createComposeRule() @Test fun showsErrorForEmptyEmail() { rule.setContent { var state by remember { mutableStateOf(LoginState()) } LoginScreen( state = state, onLogin = { state = state.copy(error = "Email required") } ) } rule.onNodeWithText("Login").performClick() rule.onNodeWithText("Email required").assertIsDisplayed() }
93

How does Compose interoperate with existing XML views?

Save to ▾
Show answer
To use Compose inside views, add a ComposeView to an XML layout or Fragment and call setContent, setting a ViewCompositionStrategy suited to the lifecycle. To use a view inside Compose, wrap it with AndroidView(factory = { MapView(it) }, update = { }). This allows migrating screen by screen, starting with new screens or leaf components. Share the theme using a Compose theme adapter or by mirroring tokens.
94

Your team wants to migrate a large XML app to Compose. How would you plan it?

Save to ▾
Show answer
Start by setting up a shared Compose theme matching the existing design system, then build new screens in Compose. Migrate leaf components and simple screens next, using ComposeView inside fragments, and leave complex custom views wrapped in AndroidView for later. Measure performance with Macrobenchmark and add Baseline Profiles. Add Compose UI and screenshot tests, train the team, and set a rule that new UI is Compose-only.
95

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

How do you make Compose UI accessible?

Save to ▾
Show answer
Give icons and images a meaningful contentDescription, or null when decorative. Keep touch targets at least 48dp, which Material components handle by default. Use semantics modifiers to merge descendants for list items, mark headings with heading(), and describe custom actions. Support font scaling by using sp and avoiding fixed heights for text. Check colour contrast and test with TalkBack and the Accessibility Scanner.
97

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

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

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

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

What is MVVM and why is it recommended for Android?

Save to ▾
Show answer
MVVM splits an app into Model (data and business logic), View (Activity, Fragment or composable that renders state) and ViewModel (holds UI state and handles user events). The ViewModel survives configuration changes and has no reference to views, so it is easy to unit test. The View observes state from the ViewModel through StateFlow or LiveData. This separation reduces bugs from lifecycle issues and keeps UI code thin.
102

Why should a ViewModel never hold a reference to an Activity, Fragment or View?

Save to ▾
Show answer
A ViewModel outlives the Activity during configuration changes. If it holds the old Activity or a view, that Activity cannot be garbage collected after rotation, causing a memory leak and possibly crashes when updating a dead view. Expose state for the UI to observe instead. If you need a Context for resources, inject the Application context or better, move that work into a repository or use resource IDs in state.
103

What is unidirectional data flow?

Save to ▾
Show answer
In unidirectional data flow, state flows down from the ViewModel to the UI, and events flow up from the UI to the ViewModel. The UI never changes state directly; it calls functions like onAddToCart(id), the ViewModel updates an immutable state object, and the UI re-renders. This makes state changes predictable, traceable and testable, and it is the model that Jetpack Compose is built around.
104

What is dependency injection and why use Hilt?

Save to ▾
Show answer
Dependency injection means classes receive their dependencies from outside rather than creating them, making code testable and loosely coupled. Hilt, built on Dagger, is Jetpack's recommended DI library. It generates code at compile time, provides standard components tied to Android lifecycles (SingletonComponent, ActivityRetainedComponent, ViewModelComponent), and integrates with ViewModels via @HiltViewModel. Errors appear at compile time rather than runtime.
105

Unit tests for a ViewModel are hard to write because it creates the repository inside itself. What do you change?

Save to ▾
Show answer
Constructor-inject the repository as an interface, so tests can pass a fake implementation. With Hilt, add @Inject to the constructor and bind the real implementation in a module. Also inject dispatchers rather than using Dispatchers.IO directly. Then tests create ViewModel(FakeRepository()) and control results precisely. This change improves design too, because the ViewModel no longer knows how data is created.
106

A new developer asks why we need interfaces for every repository. How do you answer?

Save to ▾
Show answer
Interfaces let us swap implementations, mainly fakes in tests and alternate data sources, and they define clear contracts between modules so feature modules depend on abstractions instead of concrete network code. But the rule is not absolute: if a class has one implementation and is easy to test directly, an interface adds noise. I would explain the reason with an example test and our team convention.
107

Explain the recommended layers in Android app architecture.

Save to ▾
Show answer
The official architecture guide recommends a UI layer (composables or views plus ViewModels as state holders), an optional domain layer (use cases that combine or reuse business logic), and a data layer (repositories that expose data and coordinate data sources like Room and Retrofit). Dependencies point inward: UI depends on domain or data, never the reverse. The repository is the single source of truth for each data type.
108

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

What is a repository and why is it the single source of truth?

Save to ▾
Show answer
A repository hides where data comes from and gives the rest of the app one API, such as getProducts(): Flow<List<Product>>. With an offline-first design, the repository reads from Room and refreshes from the network in the background, writing results to the database. The UI always observes the database, so it is the single source of truth. This avoids inconsistent copies of data across screens.
110

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

Write a Hilt module that provides Retrofit and a repository binding.

Save to ▾
Show answer
@Module @InstallIn(SingletonComponent::class) object NetworkModule { @Provides @Singleton fun provideRetrofit(): Retrofit = Retrofit.Builder() .baseUrl("https://api.example.com/") .addConverterFactory(Json.asConverterFactory("application/json".toMediaType())) .build() @Provides @Singleton fun provideApi(retrofit: Retrofit): ShopApi = retrofit.create(ShopApi::class.java) } @Module @InstallIn(SingletonComponent::class) abstract class RepoModule { @Binds abstract fun bindRepo(impl: ProductRepositoryImpl): ProductRepository }
112

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") } } } }
113

What is the difference between @Provides and @Binds in Hilt?

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

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

A ViewModel has grown to 1500 lines handling many responsibilities. How would you refactor it?

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

Two screens show different values for the same cart count. How do you fix the architecture?

Save to ▾
Show answer
Each screen probably fetches and caches its own copy. Create a single CartRepository, scoped as a singleton, that owns cart data in Room or an in-memory StateFlow, and have both ViewModels observe its Flow instead of keeping private copies. Mutations go through the repository so every observer updates automatically. This enforces the single source of truth and removes the need for manual refresh calls between screens.
117

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

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

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 } }
120

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

Describe a project where you improved code quality for the whole team.

Save to ▾
Show answer
Situation: our app had no shared conventions and reviews took days. Task: speed up reviews and reduce bugs. Action: I proposed ktlint and detekt in CI, created a module template with a convention plugin, and wrote a one-page guide on ViewModel and repository patterns with examples. Result: review time dropped by about half over two months, and new joiners shipped their first feature faster.
122

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

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

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

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

What is Room and what are its main components?

Save to ▾
Show answer
Room is the Jetpack persistence library built on SQLite. Its main parts are @Entity classes that map to tables, @Dao interfaces with queries annotated by @Query, @Insert, @Update and @Delete, and an abstract @Database class that ties them together. Room verifies SQL at compile time, supports suspend functions and Flow return types, and handles migrations. It removes most SQLite boilerplate while keeping full SQL power.
127

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) }
128

What is DataStore and why does it replace SharedPreferences?

Save to ▾
Show answer
Jetpack DataStore stores key-value pairs (Preferences DataStore) or typed objects (Proto DataStore) using coroutines and Flow. Unlike SharedPreferences, it is safe to call from the main thread because I/O is asynchronous, it reports errors through exceptions, and updates are transactional. SharedPreferences apply() can still block the main thread during lifecycle events and cause ANRs. Use one DataStore instance per file, ideally as a singleton.
129

Write code to save and read a dark mode preference with Preferences DataStore.

Save to ▾
Show answer
val Context.dataStore by preferencesDataStore(name = "settings") private val DARK_MODE = booleanPreferencesKey("dark_mode") class SettingsRepository(private val context: Context) { val darkMode: Flow<Boolean> = context.dataStore.data .catch { emit(emptyPreferences()) } .map { it[DARK_MODE] ?: false } suspend fun setDarkMode(enabled: Boolean) { context.dataStore.edit { it[DARK_MODE] = enabled } } }
130

What is WorkManager and when should you use it?

Save to ▾
Show answer
WorkManager schedules deferrable, guaranteed background work that must run even if the app exits or the device restarts, such as syncing data, uploading logs or backing up photos. It supports constraints like network or charging, retries with backoff, chaining and unique work. It is not for exact-time tasks or work that must finish immediately while the user waits; use coroutines for that and AlarmManager for exact alarms.
131

How do you prioritise when you have several bugs and a feature deadline in the same week?

Save to ▾
Show answer
I rank by user impact and risk: crashes and data loss first, then bugs affecting payments or login, then the feature, then minor UI issues. I share the plan with my lead and product manager so trade-offs are visible, and I ask for help or a scope cut early instead of quietly missing the deadline. Small, safe fixes I batch together to save release overhead.
132

A user complains the app uses too much storage. How do you find and reduce it?

Save to ▾
Show answer
Check Settings storage breakdown and inspect filesDir, cacheDir and databases with Device Explorer. Common culprits are unbounded image caches, old downloaded files, logs, and databases that never delete old rows. Set size limits on Coil and OkHttp caches, use cacheDir for temporary files so the system can clear them, delete old records with a periodic WorkManager job, and compress or downscale stored images.
133

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

Write a periodic sync job with WorkManager that runs only on unmetered network.

Save to ▾
Show answer
class SyncWorker(ctx: Context, params: WorkerParameters) : CoroutineWorker(ctx, params) { override suspend fun doWork(): Result = try { repo.sync() Result.success() } catch (e: IOException) { if (runAttemptCount < 3) Result.retry() else Result.failure() } } val request = PeriodicWorkRequestBuilder<SyncWorker>(6, TimeUnit.HOURS) .setConstraints(Constraints(requiredNetworkType = NetworkType.UNMETERED)) .setBackoffCriteria(BackoffPolicy.EXPONENTIAL, 30, TimeUnit.SECONDS) .build() WorkManager.getInstance(context).enqueueUniquePeriodicWork( "sync", ExistingPeriodicWorkPolicy.KEEP, request)
135

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

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

A Room query on a large table is slow and causes jank. How do you fix it?

Save to ▾
Show answer
Make sure the query runs off the main thread using suspend or Flow. Use EXPLAIN QUERY PLAN to check for full table scans, and add an @Index on columns used in WHERE, JOIN or ORDER BY. Select only needed columns into a smaller data class instead of SELECT *, page results with Paging 3, and avoid loading large blobs into lists. Wrap bulk inserts in a single transaction.
138

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

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

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

What file storage options does Android provide and how does scoped storage affect them?

Save to ▾
Show answer
App-specific storage (filesDir, cacheDir, getExternalFilesDir) needs no permission and is deleted on uninstall. Shared media is accessed through MediaStore, and other documents through the Storage Access Framework. Since Android 10, scoped storage blocks broad file system access, and since Android 13 media permissions are split into images, video and audio. The photo picker lets users choose media without any permission, which is preferred.
142

What conflict strategies does Room offer for inserts?

Save to ▾
Show answer
@Insert(onConflict = ...) supports REPLACE, which deletes the old row and inserts the new one; IGNORE, which skips the insert; ABORT, the default, which rolls back the transaction; and others. REPLACE can trigger cascading deletes on foreign keys because it deletes first. @Upsert, added in Room 2.5, inserts or updates without deleting, making it safer for syncing server data into local tables.
143

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

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()
145

Tell me about a time a bug you introduced reached production. How did you handle it?

Save to ▾
Show answer
Situation: a migration I wrote missed a default value, and some users lost their saved addresses. Task: fix it and regain trust. Action: I told my lead immediately, we paused the rollout, I wrote a correct migration with a test covering upgrades from the last three versions, and we restored data from the server for affected users. Result: impact stayed small, and migration tests became mandatory in our CI.
146

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

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

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

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

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

What is Retrofit and how does it work with coroutines?

Save to ▾
Show answer
Retrofit is a type-safe HTTP client that turns an annotated Kotlin interface into API calls. You declare methods with @GET, @POST, @Path, @Query and @Body, and Retrofit generates the implementation using OkHttp underneath. Marking methods as suspend makes them main-safe and cancellable, returning the parsed body directly or Response<T> when you need status codes and headers. A converter like kotlinx.serialization or Moshi parses JSON.
152

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 }
153

How do you check network connectivity in modern Android?

Save to ▾
Show answer
Use ConnectivityManager with registerDefaultNetworkCallback and NetworkCapabilities to check NET_CAPABILITY_INTERNET and VALIDATED, and expose it as a Flow with callbackFlow. NetworkInfo and the CONNECTIVITY_ACTION broadcast are deprecated. Remember that being connected does not guarantee a working server, so still handle request failures. Use the status to show an offline banner and pause retries, not to block every request.
154

How do you handle a situation where you are blocked by another team's API not being ready?

Save to ▾
Show answer
I agree on the API contract early, with sample JSON, and build against a fake or mock server so my UI and logic are ready. I keep the repository interface stable, so switching to the real API is a small change. I communicate status in standups and raise risks to my lead early if the dependency threatens the deadline, suggesting options like releasing behind a feature flag.
155

What are OkHttp interceptors and give common uses.

Save to ▾
Show answer
Interceptors observe or modify requests and responses in the OkHttp chain. Application interceptors run once per call and are used for adding auth headers, common query parameters or logging with HttpLoggingInterceptor. Network interceptors run for each network request including redirects and see the actual network bytes, useful for cache headers. Never log full bodies or tokens in release builds, because logs can leak sensitive data.
156

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()
157

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

How do you model API results and errors cleanly for the UI?

Save to ▾
Show answer
Map transport-level outcomes into a sealed result type in the data layer, for example Success(data), HttpError(code, message) and NetworkError. Catch IOException for connectivity, HttpException for non-2xx codes, and serialization exceptions for bad payloads, and never catch CancellationException. The ViewModel converts these into user-friendly UiState messages. Keep DTOs separate from domain models so API changes do not ripple through the UI.
159

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

The backend team changed a field from a number to a string and the app crashed for all users. How do you prevent this?

Save to ▾
Show answer
Short term, make parsing lenient for that field, for example with a custom serializer, and handle parse failures without crashing. Long term, agree on API contracts with versioning, use contract tests or a shared OpenAPI schema checked in CI, add ignoreUnknownKeys and default values in kotlinx.serialization, and use remote config or a minimum supported app version mechanism. Monitor parse errors as non-fatal events.
161

How do you handle push notifications with Firebase Cloud Messaging?

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

Images in a feed load slowly and the app runs out of memory. What do you change?

Save to ▾
Show answer
Use an image library like Coil, which handles memory and disk caching, request downsampling to the view size, and cancellation when items scroll away. Ask the backend or CDN for resized, compressed images such as WebP instead of full-size originals. Use placeholders and crossfade, avoid holding Bitmaps in ViewModels, and set memory cache limits. Profile with Memory Profiler to confirm the large allocations are gone.
163

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

Write a function that retries a suspend call with exponential backoff.

Save to ▾
Show answer
suspend fun <T> retryWithBackoff( times: Int = 3, initialDelay: Long = 500, factor: Double = 2.0, block: suspend () -> T ): T { var delayMs = initialDelay repeat(times - 1) { try { return block() } catch (e: IOException) { /* retry */ } delay(delayMs) delayMs = (delayMs * factor).toLong() } return block() }
165

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

A payment SDK integration fails only in release builds. What would you check?

Save to ▾
Show answer
First check R8 or ProGuard: release builds shrink and obfuscate code, and the SDK may need keep rules for classes used by reflection or JSON parsing. Check the SDK documentation for required rules and add them to proguard-rules.pro. Also verify the release signing key SHA is registered with the provider, the correct production keys and base URLs are used, and network security config allows the domains.
167

Explain the network security configuration file.

Save to ▾
Show answer
network_security_config.xml, referenced from the manifest, controls TLS behaviour without code. Since Android 9, cleartext HTTP is blocked by default; you can allow it for specific debug domains. You can trust user-added CAs only in debug builds for tools like Charles or proxies, and define certificate pins with expiry. Keeping cleartext disabled in production is an important security baseline.
168

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

Describe a situation where you had to push back on an unrealistic deadline.

Save to ▾
Show answer
Situation: product wanted offline mode in one week. Task: give a realistic plan. Action: I broke the work into tasks with estimates, showed that sync conflicts and migrations alone needed two weeks, and proposed a smaller first release with read-only offline caching in one week. Result: we shipped the smaller version on time and the full offline sync three weeks later without quality issues.
170

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

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

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

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

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

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

What types of tests do you write for an Android app?

Save to ▾
Show answer
Local unit tests on the JVM cover ViewModels, use cases, repositories and mappers with JUnit, fakes and coroutine test tools; they are fast and most numerous. Instrumented tests run on a device or emulator for Room, Compose UI and integration flows. End-to-end UI tests check critical journeys like login and checkout. Screenshot tests catch visual regressions. Follow the testing pyramid: many unit tests, fewer UI tests.
177

What is an ANR and how do you prevent it?

Save to ▾
Show answer
An ANR (Application Not Responding) happens when the main thread is blocked, for example an input event not handled within about 5 seconds or a BroadcastReceiver taking too long. Causes include disk or network work on the main thread, heavy computation, lock contention and slow SharedPreferences commits. Prevent ANRs by moving work to background dispatchers, using StrictMode in debug builds, and tracking ANR rates in Android vitals.
178

What is an Android App Bundle and why does Play require it?

Save to ▾
Show answer
An App Bundle (.aab) is a publishing format containing all compiled code and resources. The Play Store generates optimised APKs for each device configuration, delivering only the needed screen density, CPU architecture and language resources, which reduces download size. Since August 2021, new apps on Play must use AAB with Play App Signing, where the store holds the app signing key and you sign uploads with an upload key.
179

Explain the steps to publish an app on the Play Store.

Save to ▾
Show answer
Create a developer account, then an app in Play Console. Complete the store listing, content rating, data safety form, target audience, privacy policy and permissions declarations. Build a signed release AAB with a higher versionCode, upload it to internal testing first, then closed or open testing, and finally production with a staged rollout. New personal accounts must run a closed test with at least 12 testers for 14 days before production.
180

Tell me about an app or feature you are proud of building.

Save to ▾
Show answer
Choose something with clear impact. For example: I built the offline cart for our grocery app. Users in low network areas could add items without connection, and the cart synced later with conflict handling. I used Room, WorkManager and a sync status field, and wrote migration and sync tests. After release, cart abandonment from network errors dropped noticeably, and the pattern was reused for the wishlist feature.
181

Where do you see yourself in five years as an Android developer?

Save to ▾
Show answer
Keep it genuine and tied to growth. For example: in five years I want to be a senior or lead Android engineer who owns the architecture of a large app, mentors juniors and works closely with product on decisions. I plan to deepen my skills in performance, Compose and Kotlin Multiplatform, and contribute to the team's engineering practices. I hope to grow that path in a company like this one.
182

How do you handle a code review comment you disagree with?

Save to ▾
Show answer
I first assume the reviewer has a reason and ask questions to understand it. If I still disagree, I explain my reasoning with evidence, such as documentation, a benchmark or a test, and suggest a short call if comments go back and forth. If it is a matter of preference, I follow the team convention. The goal is the best code for the team, not winning the argument.
183

Write a unit test for a ViewModel using a fake repository and runTest.

Save to ▾
Show answer
class ProductViewModelTest { @get:Rule val mainRule = MainDispatcherRule() @Test fun loadsProductsIntoState() = runTest { val fake = FakeProductRepository(listOf(Product("1", "Pen", 10.0))) val vm = ProductViewModel(fake) advanceUntilIdle() val state = vm.state.value assertFalse(state.loading) assertEquals(1, state.items.size) assertNull(state.error) } }
184

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

What does R8 do in a release build?

Save to ▾
Show answer
R8 shrinks code by removing unused classes and members, optimises bytecode by inlining and removing dead branches, obfuscates names to make reverse engineering harder and reduce size, and shrinks resources when enabled. It needs keep rules for code accessed by reflection, such as some serialization or SDK classes. Upload the mapping file to Play Console and Crashlytics so obfuscated stack traces can be decoded.
186

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

Write a Gradle configuration for release signing and minification using environment variables.

Save to ▾
Show answer
android { signingConfigs { create("release") { storeFile = file(System.getenv("KEYSTORE_PATH") ?: "release.jks") storePassword = System.getenv("KEYSTORE_PASSWORD") keyAlias = System.getenv("KEY_ALIAS") keyPassword = System.getenv("KEY_PASSWORD") } } buildTypes { release { isMinifyEnabled = true isShrinkResources = true proguardFiles(getDefaultProguardFile("proguard-android-optimize.txt"), "proguard-rules.pro") signingConfig = signingConfigs.getByName("release") } } }
188

How do you set up CI/CD for an Android app?

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

The crash-free rate dropped after a release only on Android 15 devices. How do you respond?

Save to ▾
Show answer
Halt the staged rollout immediately. Filter Crashlytics or Play vitals by OS version to find the top crash, then check Android 15 behaviour changes such as edge-to-edge enforcement, foreground service rules or new restrictions affecting your target SDK. Reproduce on an Android 15 emulator, fix, add a test, and resume rollout with the hotfix. Afterward, add Android 15 devices to the regular test matrix.
190

The Play Store rejected your update for a policy violation about permissions. What do you do?

Save to ▾
Show answer
Read the rejection email and policy centre message carefully to see which permission or declaration failed, often SMS, call log, background location, all-files access or exact alarms. Remove the permission if a less sensitive alternative exists, such as the SMS Retriever API or photo picker. Otherwise update the permission declaration form with a clear use case and video. Check the merged manifest for permissions added by libraries.
191

The APK size grew to 120 MB and installs from small towns are dropping. How do you reduce it?

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

QA says a bug happens only on one manufacturer's phones. How do you debug it?

Save to ▾
Show answer
Get exact device model, OS and skin versions, and steps. Use a remote device lab or Firebase Test Lab to reproduce if you lack the device. Collect logcat and bug reports, and compare behaviour around known differences like battery optimisation, autostart restrictions, custom permission dialogs or WebView versions. Add targeted logging or a non-fatal Crashlytics event, then apply a fix guarded by checks only if necessary.
193

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

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

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

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

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

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

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

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

What jobs can I get after learning Android App Development?

You can apply for roles such as Junior Android Developer, Android Developer, Kotlin Developer, Mobile Engineer and Android UI Engineer. With experience you can grow into Senior Android Engineer, Android Tech Lead or Mobile Architect. Employers include e-commerce, fintech, food delivery, banking, telecom and edtech companies, plus IT services firms building apps for global clients. A strong portfolio of published apps and clean GitHub code matters as much as your degree when applying.

Who is eligible to learn Android development?

Anyone with basic computer skills and logical thinking can start. B.Tech, BCA, MCA, B.Sc and diploma students are common learners, and working professionals from testing, support or web development also switch successfully. Prior programming helps but is not required, because the course starts with Kotlin basics. You need a laptop with at least 8 GB RAM, ideally 16 GB, to run Android Studio and the emulator smoothly.

How long does it take to become job-ready in Android development?

With consistent daily practice of two to three hours, most learners become job-ready for junior roles in about four to six months. The 16-week course covers the core skills, and the remaining time goes into building two or three portfolio apps and interview preparation. People with prior programming experience often move faster. Becoming confident with architecture, performance and production issues takes one to two years of real project work.

Should I learn Kotlin or Java for Android in 2026?

Learn Kotlin. Google has been Kotlin-first since 2019, Jetpack Compose is Kotlin-only, and almost all new Android code and official samples use Kotlin. Java knowledge is still useful because many older codebases and libraries use it, and Kotlin interoperates fully with Java, so you can read and maintain such code later. Interviews for Android roles in India now expect Kotlin, coroutines and Compose.

Is there a certification for Android developers?

Google retired its Associate Android Developer exam, so there is currently no official Google certification widely required by employers. Companies judge Android candidates mainly through portfolio apps, GitHub code, live coding and technical interviews. A course completion certificate shows structured learning, but published apps on the Play Store with good architecture and tests are the strongest proof of skill you can show recruiters.

Is native Android still worth learning when cross-platform tools exist?

Yes. Large consumer apps with complex features, heavy performance needs or deep hardware use usually stay native, and many companies hire dedicated Android engineers. Cross-platform tools like Flutter and React Native are popular too, but knowing native Android makes you better at those, because you understand lifecycles, permissions and performance underneath. Kotlin Multiplatform also lets Android developers share code with iOS, extending the value of Kotlin skills.