---
title: Android Developer Interview Questions (2026)
description: Android developer interview questions for 2026 — lifecycle, Compose vs XML, coroutines, ViewModel/StateFlow, memory leaks, DI — with real Kotlin code.
url: https://usegreenroom.app/blog/android-developer-interview-questions
last_updated: 2026-06-20
---

← Back to blog

Roles

# Android developer interview questions

June 20, 2026 · 35 min read

![Android developer interview questions and answers — cover from Greenroom, the AI mock interviewer](/assets/blog/android-developer-interview-questions-hero.webp)

It's 11:40pm, the night before a Bangalore fintech's L2 Android round, and you are doing the thing every Android developer has done at least once: rebuilding a side-project app from six months ago because the interviewer's job description mentioned "experience with modern Android architecture" and you have a sudden, deep need to remember what `onSaveInstanceState` actually does. Your emulator is the temperature of a space heater. You rotate the device to test a config change, the app crashes, and for one heart-stopping second you consider whether you can convincingly claim this was intentional, a live demo of "what *not* to do," a teaching moment, ha ha, very on purpose.

It was not on purpose. Your `ViewModel` was holding a reference to the old `Activity` context, the configuration change recreated the activity, and now you've got a leak and a stack trace that looks like a ransom note. You fix it at 12:15am by moving the dependency into the `ViewModel`'s constructor instead of passing it in from `onCreate`, mutter "I knew that," and go to sleep mildly betrayed by your own code.

The next day, the interviewer's first question is: "Can you walk me through what happens to an activity when the user rotates the screen?" You answer fine — you literally debugged this twelve hours ago — but you answer like someone reciting something they memorized under duress rather than someone who understands it, because that is, structurally, exactly what happened. The follow-up question — "okay, so where specifically would a memory leak sneak in during that process?" — catches you a half-second flat-footed, because you fixed the bug with your hands before you'd explained it with your mouth, and those are different muscles.

This guide exists for that gap. It covers the **Android developer interview questions** that actually come up in 2026 — the activity and fragment lifecycle, Jetpack Compose vs XML views, Kotlin coroutines, ViewModel/LiveData/StateFlow, memory leaks, MVVM and architecture, and dependency injection with Hilt/Dagger — with real Kotlin code, not just bullet-point definitions. Treat it as a script for explaining out loud what you already half-know with your hands.

## Why Android interviews are mostly the same five topics in a different order

Strip away the company-specific trivia and almost every Android interview, from a 200-person Series B startup to a TCS digital unit to a FAANG-adjacent product team, is testing five things: do you understand the lifecycle (because everything else builds on it), can you reason about state and concurrency without crashing the main thread, do you know *why* the current architecture conventions exist (not just their names), can you avoid the classic memory-leak traps, and can you talk about Compose vs the View system like someone who has shipped both, not someone who read a migration blog post once.

That's genuinely most of it. The differences between companies are usually depth (a senior round expects you to whiteboard a custom `CoroutineScope` cancellation strategy; a fresher round is happy if you can correctly name the lifecycle callbacks in order) and which corner of Jetpack they personally care about (a Room-heavy team will dig into database migrations; a Compose-first team will probe recomposition and `remember`). The core five rarely change, which is good news: it means a few weeks of focused, *verbal* practice covers most of what you'll actually face.

## The activity lifecycle — the question that opens almost every round

The activity lifecycle gets asked first disproportionately often, partly because it's foundational (you can't reason about ViewModel survival, state restoration, or memory leaks without it) and partly because it's a great filter — it's simple enough that everyone can recite the callback names, and revealing enough that follow-ups quickly separate "memorized the diagram" from "has actually debugged a lifecycle bug."

The seven callbacks, in the order a normal launch-to-finish hits them:

```kotlin
class MainActivity : AppCompatActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        // one-time setup: inflate layout, restore saved state, set up ViewModel
    }
    override fun onStart() {
        super.onStart()
        // activity becomes visible (not yet interactive)
    }
    override fun onResume() {
        super.onResume()
        // activity is now in the foreground and interactive
    }
    override fun onPause() {
        super.onPause()
        // losing focus — pause animations, release camera, write small state fast
    }
    override fun onStop() {
        super.onStop()
        // fully hidden — release heavier resources, stop location updates
    }
    override fun onDestroy() {
        super.onDestroy()
        // final cleanup — unregister listeners, cancel coroutines tied to this activity
    }
    override fun onRestart() {
        super.onRestart()
        // called before onStart when coming back from onStop, not from a fresh launch
    }
}
```

The interview-useful framing isn't memorizing the names — it's knowing what to *do* in each one. `onPause` needs to be fast, because the system may be waiting on it before showing another app (a dialing screen, a permission prompt); heavy work belongs in `onStop`. `onDestroy` isn't guaranteed to run at all on a low-memory kill — the system can just evict your process — which is precisely why you can't rely on it for anything that must always happen (saving critical data belongs earlier, in `onPause` or via `onSaveInstanceState`).

**The follow-up that catches people:** "What happens differently if the user presses Home vs presses Back?" Home triggers `onPause` → `onStop`, and the activity stays in memory (it can be brought back to `onRestart` → `onStart` → `onResume`). Back calls `finish()`, which runs the full `onPause` → `onStop` → `onDestroy` sequence, and the activity is gone for good — pressing it again from the recent-apps list (if visible) means a fresh `onCreate`. Knowing this distinction out loud, without hedging, is the single fastest way to sound like you've actually used Android's lifecycle to debug something, rather than read about it.

### Configuration changes and why your activity gets destroyed mid-rotation

By default, rotating the screen (or toggling dark mode, or changing the system font size) destroys and recreates the activity — `onPause`, `onStop`, `onDestroy`, then a fresh `onCreate` with a new `Bundle` containing whatever you saved. This is *intentional*, not a bug Android forgot to fix: a new configuration can need a completely different layout (a different XML resource for landscape, different drawable density), so the platform's default answer is "just rebuild it."

The practical consequence interviewers want you to state plainly: don't store UI state only in the activity instance — it dies on rotation. Use `onSaveInstanceState` for small, serializable state (scroll position, a typed-but-not-submitted form value) and a `ViewModel` for state that should survive the recreation cleanly without round-tripping through a `Bundle`:

```kotlin
class SearchActivity : AppCompatActivity() {
    override fun onSaveInstanceState(outState: Bundle) {
        super.onSaveInstanceState(outState)
        outState.putString("query", currentQuery)
    }
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        currentQuery = savedInstanceState?.getString("query") ?: ""
    }
}
```

You can also opt out of the destroy-and-recreate dance entirely with `android:configChanges="orientation|screenSize"` in the manifest, handling the change yourself in `onConfigurationChanged` — but most teams avoid this, because it's easy to miss a resource that *should* change with orientation and end up with stale layouts. The ViewModel-survives-rotation pattern below is the actual modern answer, and it's worth being able to explain *why* it works, not just that it does.

## Fragments, and why "activity vs fragment" is really a question about hosting

A `Fragment` is a reusable piece of UI and behavior that lives inside a host (usually an `Activity`, sometimes another `Fragment`). It has its own lifecycle that's layered on top of — and partly driven by — its host's lifecycle, which is exactly why the fragment lifecycle has *more* callbacks than the activity one: it needs hooks for view creation/destruction independent of the fragment object itself, because a fragment's view can be destroyed and recreated (e.g., when it's removed from the back stack and re-added) while the fragment instance survives.

The fragment-specific callbacks worth being able to name in order: `onAttach` (fragment is attached to its host context), `onCreate` (fragment-level, not view-level, initialization), `onCreateView` (inflate and return the fragment's View), `onViewCreated` (the view hierarchy now exists — safe to find views and bind data here), `onStart`/`onResume` (mirror the host), `onPause`/`onStop` (mirror the host), `onDestroyView` (the view is being torn down — release any view references here to avoid leaking the view binding), `onDestroy`, `onDetach`.

The classic interview trap is **not nulling out a `ViewBinding` reference in `onDestroyView`**, which is one of the most common real-world Android memory leaks:

```kotlin
class ProfileFragment : Fragment() {
    private var _binding: FragmentProfileBinding? = null
    private val binding get() = _binding!!

    override fun onCreateView(
        inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?
    ): View {
        _binding = FragmentProfileBinding.inflate(inflater, container, false)
        return binding.root
    }

    override fun onDestroyView() {
        super.onDestroyView()
        _binding = null // critical — the fragment instance can outlive its view
    }
}
```

Without that null-out, the fragment (which the back stack is still holding a reference to) keeps a strong reference to its old, destroyed view hierarchy through the binding object, and that view holds references to its own subviews and listeners — a leak that's invisible until you profile memory on a screen the user navigates to and from repeatedly, like a bottom-nav tab.

**Activity vs Fragment, the answer interviewers actually want:** an `Activity` is an entry point the OS knows about and can launch independently (it's declared in the manifest, has its own task/back-stack semantics at the OS level); a `Fragment` has no independent existence — it's always hosted, exists to make UI modular and reusable across activities, and its back stack is managed by your app's `FragmentManager`, not the OS. Modern apps tend toward a single-activity architecture with Jetpack Navigation moving between fragments (or, increasingly, Compose screens) precisely because fragment-level navigation is cheaper and more flexible than juggling multiple activities.

## Intents, Services, Broadcast Receivers, and Context — the four words that confuse Android newcomers most

**Intents** are messages — "do this" (explicit Intent naming a specific component, like launching `DetailsActivity`) or "something can do this" (implicit Intent, like "open a URL," resolved by the system to whichever app handles it). They carry data via extras and are how Android components talk to each other across process boundaries, which is also why they're how deep links and inter-app sharing work.

**Services** run work in the background, without a UI, that should continue even if the user leaves your activity — think a music player or a sync job. The interview-critical nuance for 2026: starting a long-running foreground `Service` now requires a visible notification and, on recent Android versions, an explicitly declared foreground service *type* (`location`, `mediaPlayback`, `dataSync`, etc.) in the manifest — Google has been steadily tightening what background work apps can silently do, which is exactly why **WorkManager** (covered below) is now the default answer for most "do this later, reliably" use cases rather than a raw `Service`.

**Broadcast Receivers** listen for system-wide or app-wide events — battery low, connectivity changed, an app-defined custom broadcast — and react. They're less central than they used to be because Android has restricted what *implicit* broadcasts apps can register for in the background (a battery and security trade-off), pushing more of this work toward `JobScheduler`/`WorkManager` constraints instead of "wake up whenever this broadcast fires."

**Context** is, frankly, the most-misunderstood four-syllable word in Android. It's the interface to global app state and resources — string lookups, starting activities/services, inflating layouts, accessing system services. The interview-relevant distinction: `Application` context lives as long as the app process and is safe to hold a long-lived reference to; `Activity` context is tied to that specific activity instance and is destroyed and recreated with it — **holding an `Activity` context in a singleton, a static field, or a long-lived `ViewModel` dependency is one of the most common memory leak sources in real Android codebases**, because it pins the entire activity (and its view tree) in memory long after the activity itself should have been garbage collected.

## MVVM, MVP, MVC — and why interviewers keep asking "why," not "what"

Almost every Android candidate can recite that MVVM stands for Model-View-ViewModel. Far fewer can explain *why it beat MVP and MVC* for Android specifically, which is the actual question being asked underneath "explain MVVM."

**MVC (Model-View-Controller)**, the oldest pattern, splits UI (View), data (Model), and the glue logic that updates one from the other (Controller). On Android, the View and Controller roles blur badly — your `Activity`/`Fragment` often ends up doing both, because it's simultaneously the View (inflating XML) and the de facto Controller (handling click listeners, deciding what to show). That blurring is exactly why MVC fell out of favor for Android specifically — not because the pattern is wrong in the abstract, just a poor fit for a platform where the View class is also the lifecycle-owning class.

**MVP (Model-View-Presenter)** fixed the blurring by introducing a `Presenter` that holds all the logic and talks to a thin `View` interface (implemented by the Activity/Fragment) purely through method calls — `view.showLoading()`, `view.showError(message)`. This is genuinely more testable than MVC (you can unit test the Presenter with a mock View), but it has a structural weakness: the Presenter has no lifecycle awareness of its own, so you end up manually tracking whether the View is still attached before calling its methods, to avoid crashing on a callback that arrives after the Activity is destroyed.

**MVVM (Model-View-ViewModel)** solves exactly that weakness by making the `ViewModel` lifecycle-aware via Jetpack's `ViewModel` class (more below) and having the View *observe* state rather than being *told* what to do. The ViewModel exposes state as `LiveData` or `StateFlow`; the View subscribes and updates itself reactively. Nobody needs to manually check "is the view still attached" — if it's gone, it's simply not observing anymore, and the system handles the rest. That's the real, defensible reason MVVM is standard: it's the pattern that plays best with Android's actual lifecycle and Jetpack's first-party tooling, not an arbitrary fashion choice.

<div class="verdict"><strong>The core truth:</strong> "Explain MVVM" is rarely actually about MVVM. It's a proxy question for "do you understand *why* Android's lifecycle makes naive architectures leak and crash, and that the View/ViewModel/Model split exists to route around that, not because a blog post said so."</div>

## ViewModel, LiveData, and StateFlow — and why ViewModel actually survives rotation

This is the single most-asked architecture follow-up, and the honest answer surprises some candidates: a `ViewModel` survives a configuration change **not** because of any special lifecycle magic inside the `ViewModel` class itself, but because the `ViewModelStore` holding it is retained by the `FragmentManager`/`Activity` machinery across the destroy-recreate cycle, and the *new* activity instance is handed back the *same* `ViewModelStore` (and therefore the same `ViewModel` instance) rather than getting a fresh one. The `ViewModel` is destroyed only when the activity finishes for real (the user navigates away permanently), not when it's merely recreated for a config change.

```kotlin
class ProfileViewModel(private val repo: ProfileRepository) : ViewModel() {
    private val _uiState = MutableStateFlow<ProfileUiState>(ProfileUiState.Loading)
    val uiState: StateFlow<ProfileUiState> = _uiState.asStateFlow()

    init {
        viewModelScope.launch {
            _uiState.value = try {
                ProfileUiState.Success(repo.getProfile())
            } catch (e: IOException) {
                ProfileUiState.Error(e.message ?: "Unknown error")
            }
        }
    }
}

sealed interface ProfileUiState {
    object Loading : ProfileUiState
    data class Success(val profile: Profile) : ProfileUiState
    data class Error(val message: String) : ProfileUiState
}
```

The `Activity`/`Fragment` then just observes:

```kotlin
class ProfileFragment : Fragment() {
    private val viewModel: ProfileViewModel by viewModels()

    override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
        super.onViewCreated(view, savedInstanceState)
        viewLifecycleOwner.lifecycleScope.launch {
            viewModel.uiState.collect { state ->
                when (state) {
                    is ProfileUiState.Loading -> binding.progress.isVisible = true
                    is ProfileUiState.Success -> renderProfile(state.profile)
                    is ProfileUiState.Error -> showError(state.message)
                }
            }
        }
    }
}
```

**LiveData vs StateFlow — the question that signals whether you're current.** `LiveData` is lifecycle-aware out of the box and automatically stops delivering updates to a destroyed observer, which made it the safe default for years. `StateFlow` (from Kotlin coroutines) always has a current value, composes naturally with the rest of the coroutines/Flow ecosystem (you can `map`, `combine`, `debounce` it), and is the more idiomatic choice in a coroutines-first, increasingly Compose-first codebase — but it isn't lifecycle-aware on its own, which is why you collect it with `repeatOnLifecycle(Lifecycle.State.STARTED)` or `viewLifecycleOwner.lifecycleScope` rather than a raw, unscoped `collect`, to avoid collecting (and doing work) while the UI is in the background.

The honest, balanced answer for "which should I use" — the one that signals real judgment rather than parroting a trend — is: `StateFlow` for new code, especially anything that will eventually move toward Compose, because it composes better with coroutines and testing; `LiveData` is still completely fine in mature XML-based codebases where rewriting working code for its own sake isn't worth the churn. Greenfield, coroutines-first: StateFlow. Maintaining a five-year-old XML app: don't rip out working LiveData just to chase a trend.

## Jetpack Compose vs XML views — the "which world do you actually live in" question

By 2026 almost every team has *an opinion* on Compose, and the interview question is rarely "what is Compose" — it's "have you actually built something nontrivial in it, and do you understand what changed under the hood, not just the new syntax."

**XML + View system**, the original model, is imperative: you inflate a layout, get references to views (`findViewById` or, much more commonly now, generated `ViewBinding`), and manually push updates — `binding.title.text = newTitle`. It's verbose but extremely predictable, has the deepest tooling/library ecosystem, and is what most large, older Android codebases are still built on (which is exactly why it still gets asked about — most jobs aren't greenfield).

**Compose** is declarative: you describe *what* the UI should look like for a given state, and the framework figures out what changed and re-renders ("recomposes") only the parts that need it:

```kotlin
@Composable
fun ProfileScreen(viewModel: ProfileViewModel = viewModel()) {
    val uiState by viewModel.uiState.collectAsStateWithLifecycle()

    when (val state = uiState) {
        is ProfileUiState.Loading -> CircularProgressIndicator()
        is ProfileUiState.Success -> ProfileContent(state.profile)
        is ProfileUiState.Error -> ErrorMessage(state.message)
    }
}

@Composable
fun ProfileContent(profile: Profile) {
    Column(modifier = Modifier.padding(16.dp)) {
        Text(text = profile.name, style = MaterialTheme.typography.headlineSmall)
        Spacer(modifier = Modifier.height(8.dp))
        Text(text = profile.bio, style = MaterialTheme.typography.bodyMedium)
    }
}
```

The interview-meaningful concept underneath the syntax is **recomposition**, and specifically when it happens unnecessarily. A composable re-runs ("recomposes") when the state it reads changes — but a *lambda passed as a parameter that's recreated on every parent recomposition* defeats Compose's ability to skip unchanged subtrees, the same way an unstable prop reference defeats `React.memo`. The fix pattern is the same family of idea as React's `useCallback`: hoist state, keep lambdas stable, and use `remember`/`derivedStateOf` for expensive computed values so they aren't recalculated every recomposition:

```kotlin
@Composable
fun SearchBar(query: String, onQueryChange: (String) -> Unit) {
    // a new lambda literal here on every parent recomposition would
    // make this composable un-skippable even when `query` hasn't changed
    TextField(value = query, onValueChange = onQueryChange)
}
```

**The honest, current-state answer for "Compose or XML":** new screens and new apps default to Compose now — Google's own tooling, samples, and Material 3 components are Compose-first, and the gap in maturity that existed in 2021-2022 has mostly closed. But a huge amount of real, revenue-generating Android code is still XML, interop between the two ("Compose inside a Fragment via `ComposeView`," or "an XML view inside Compose via `AndroidView`") is common and expected to work cleanly, and claiming zero XML experience when a JD says "5+ years Android" is its own small red flag — most senior candidates have lived in both worlds, and saying so plainly is more credible than overclaiming Compose purity.

## Kotlin coroutines — the concurrency model every Android interview now assumes

Coroutines are Kotlin's answer to async work without callback pyramids or manually managed threads. The unit of work is a `suspend` function — one that can pause without blocking the thread it's running on, and resume later:

```kotlin
class ProfileRepository(private val api: ApiService, private val dao: ProfileDao) {
    suspend fun getProfile(): Profile {
        return try {
            val fresh = api.fetchProfile() // suspends, doesn't block the thread
            dao.save(fresh)
            fresh
        } catch (e: IOException) {
            dao.getCached() ?: throw e
        }
    }
}
```

**Structured concurrency** is the actual concept interviewers are testing for, not just "do you know the keyword `suspend`." Every coroutine launches inside a `CoroutineScope`, and that scope ties the coroutine's lifetime to something meaningful — `viewModelScope` cancels automatically when the ViewModel is cleared; `lifecycleScope` cancels with the lifecycle owner. This is the structural fix for the exact class of bug that used to plague raw-thread Android code: a callback arriving after the screen is gone and crashing on a null view, or silently leaking work that nobody ever cancels.

```kotlin
class SearchViewModel : ViewModel() {
    private var searchJob: Job? = null

    fun onQueryChanged(query: String) {
        searchJob?.cancel() // cancel the previous in-flight search — same idea as
                            // AbortController in the frontend typeahead pattern
        searchJob = viewModelScope.launch {
            delay(300) // debounce
            val results = repo.search(query)
            _results.value = results
        }
    }
}
```

**Dispatchers** decide which thread pool a coroutine actually runs on: `Dispatchers.Main` for UI work (anything touching views must run here), `Dispatchers.IO` for blocking I/O (network, disk, database), `Dispatchers.Default` for CPU-heavy work (sorting/parsing large data) that shouldn't compete with UI rendering on the main thread. `withContext(Dispatchers.IO) { ... }` is the standard way to hop dispatchers mid-coroutine without manually managing thread handoffs:

```kotlin
suspend fun loadAndParseLargeFile(uri: Uri): List<Record> = withContext(Dispatchers.IO) {
    val raw = readFile(uri) // blocking I/O, fine on Dispatchers.IO
    withContext(Dispatchers.Default) {
        parseRecords(raw) // CPU-bound parsing, fine on Dispatchers.Default
    }
}
```

**Flow** is coroutines' answer to a stream of values over time rather than a single suspend-and-return result — think of it as the coroutine-native equivalent of an RxJava `Observable`, and exactly what `StateFlow` is built on. `Flow` is cold (it doesn't run until collected) and composes with the same operators you'd expect — `map`, `filter`, `debounce`, `combine` — which is why most modern Android networking + caching layers expose a `Flow<Data>` from the repository all the way up to the ViewModel's `StateFlow`.

**The interview-favorite concurrency bug to be ready to spot:** calling a `suspend` function from inside a `try` block but forgetting that `CancellationException` is a normal, expected part of structured concurrency — and that a broad `catch (e: Exception)` will accidentally swallow it, breaking cancellation propagation up the coroutine hierarchy:

```kotlin
// WRONG — swallows CancellationException, breaking structured cancellation
viewModelScope.launch {
    try {
        val data = repo.fetchData()
        _state.value = data
    } catch (e: Exception) {
        _state.value = Error(e.message)
    }
}

// RIGHT — let CancellationException propagate, only catch real failures
viewModelScope.launch {
    try {
        val data = repo.fetchData()
        _state.value = data
    } catch (e: CancellationException) {
        throw e
    } catch (e: Exception) {
        _state.value = Error(e.message)
    }
}
```

Being able to explain *why* the broad catch is wrong — not just that it is — is exactly the kind of follow-up that separates "has read about coroutines" from "has debugged a coroutine in production."

## The main thread, ANRs, and why "don't block the main thread" is the platform's first commandment

Android renders UI on a single thread (the main/UI thread), and the system watches it: if it can't respond to an input event within roughly 5 seconds, or a `BroadcastReceiver` doesn't finish within roughly 10 seconds, the OS shows the user an "Application Not Responding" dialog and offers to kill your app. An ANR is, structurally, just "you blocked the thread that draws the screen and handles input for too long" — a synchronous network call, a large JSON parse, a tight loop over a big list, all done directly inside a click handler or `onCreate`.

The fix is always some version of "move the work off the main thread" — a coroutine on `Dispatchers.IO`/`Dispatchers.Default`, as above — and the interview signal interviewers listen for is whether you can *name* a concrete ANR you've actually hit (a SharedPreferences read that got slow as the file grew, a Room query run synchronously instead of as a `suspend fun`, a bitmap decode on click) rather than reciting the definition in the abstract.

## Memory leaks — the question with the most "I've actually shipped this" signal

Android memory leaks follow a small number of repeating shapes, and being able to name them concretely (with the fix) is worth more in an interview than a general definition of "a memory leak is when memory isn't released."

**1. Holding an Activity context where you should hold an Application context** — covered above, the classic "static singleton holds onto the Activity" leak.

```kotlin
// LEAKS — the singleton now pins this Activity in memory forever
object ImageLoader {
    lateinit var context: Context
}
// MainActivity.onCreate: ImageLoader.context = this  // BUG

// FIX — use applicationContext, which lives for the process lifetime anyway
object ImageLoader {
    lateinit var context: Context
}
// MainActivity.onCreate: ImageLoader.context = applicationContext.applicationContext
```

**2. Not unregistering listeners/callbacks.** A `LocationListener`, a `BroadcastReceiver`, an `RxJava`/coroutine `Flow` collector started in `onStart` but never cancelled in `onStop` — the registered object holds a reference back to the activity/fragment, keeping it alive past when it should be collected. The fix is always symmetry: whatever you register in `onStart`, unregister in `onStop`; whatever you start in `onCreate`, cancel in `onDestroy`.

**3. The ViewBinding-after-onDestroyView leak**, shown earlier — the single most common one in fragment-heavy, bottom-nav-style apps, because the fragment instance is genuinely kept around by the back stack while only its *view* is meant to be torn down.

**4. Inner classes and anonymous listeners that implicitly hold an outer-class reference.** A non-static inner class (or, in Kotlin, a lambda that captures `this`) implicitly captures a reference to its enclosing instance — if that listener outlives the activity (e.g., it's handed to a long-lived static event bus), the activity leaks with it. The fix: prefer `WeakReference` for long-lived listener registrations, or make sure anything holding the listener has a matching unregister step tied to the same lifecycle.

**How you'd actually *find* one in the field** is itself a fair interview question: Android Studio's **Profiler** (memory tab, capture a heap dump, force a GC, look for instance counts that should be zero but aren't after navigating away from a screen) or **LeakCanary** (drop it in as a debug dependency, navigate around the app, and it automatically detects and reports retained objects with their reference chain). Naming LeakCanary specifically, and describing once having read one of its reference-chain reports, is a strong, concrete signal.

## RecyclerView and efficient lists

`RecyclerView` is the standard for any scrolling list, and its core efficiency trick is *view recycling*: instead of creating a new view for every item as the user scrolls, it keeps a small pool of views just large enough to cover what's on screen (plus a little buffer) and rebinds them with new data as items scroll in and out — `onCreateViewHolder` runs rarely, `onBindViewHolder` runs constantly.

```kotlin
class MessageAdapter(
    private var items: List<Message>
) : RecyclerView.Adapter<MessageAdapter.MessageViewHolder>() {

    class MessageViewHolder(val binding: ItemMessageBinding) :
        RecyclerView.ViewHolder(binding.root)

    override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): MessageViewHolder {
        val binding = ItemMessageBinding.inflate(
            LayoutInflater.from(parent.context), parent, false
        )
        return MessageViewHolder(binding)
    }

    override fun onBindViewHolder(holder: MessageViewHolder, position: Int) {
        val message = items[position]
        holder.binding.text.text = message.body
        holder.binding.timestamp.text = message.formattedTime
    }

    override fun getItemCount() = items.size
}
```

The interview-favorite efficiency follow-up is **`DiffUtil`**: instead of calling `notifyDataSetChanged()` (which rebinds every visible item, even unchanged ones, and skips Android's built-in change animations), you give `DiffUtil` the old list and new list, and it computes the minimal set of inserts/removes/moves, animating only what actually changed:

```kotlin
class MessageDiffCallback(
    private val old: List<Message>, private val new: List<Message>
) : DiffUtil.Callback() {
    override fun getOldListSize() = old.size
    override fun getNewListSize() = new.size
    override fun areItemsTheSame(oldPos: Int, newPos: Int) =
        old[oldPos].id == new[newPos].id
    override fun areContentsTheSame(oldPos: Int, newPos: Int) =
        old[oldPos] == new[newPos]
}
```

`ListAdapter` wraps this pattern so most apps never hand-roll the `DiffUtil.Callback` boilerplate — worth mentioning if asked, since it shows you know the modern, less-verbose path, not just the mechanism underneath it.

## Jetpack components beyond ViewModel — Room, Navigation, WorkManager

**Room** is Jetpack's SQLite abstraction — you define entities and DAOs with annotations, and Room generates the boilerplate, validates your SQL at compile time, and (critically for the architecture conversation above) exposes query results as `Flow` so your repository layer can stream database changes straight into a `StateFlow` the UI observes:

```kotlin
@Entity
data class Note(@PrimaryKey val id: Long, val title: String, val body: String)

@Dao
interface NoteDao {
    @Query("SELECT * FROM Note ORDER BY id DESC")
    fun observeAll(): Flow<List<Note>>

    @Insert
    suspend fun insert(note: Note)
}
```

The interview-relevant nuance: Room migrations. If you ship a schema change without a `Migration` (or `fallbackToDestructiveMigration()`, which deletes all local data — fine for some apps, catastrophic for others), existing users' app updates will crash on launch. Being able to say "I'd write an explicit `Migration` object with the `ALTER TABLE` SQL, version it, and test the upgrade path before shipping" is a real signal of having shipped a schema change to production users, not just a fresh-install demo.

**Navigation Component** centralizes how screens (fragments, or Compose destinations via Navigation-Compose) connect, replacing manual `FragmentTransaction` calls with a declared graph, type-safe argument passing, and automatic back-stack and deep-link handling — the main thing it buys you is not having to hand-write fragment transaction code at every navigation call site.

**WorkManager** is the modern default for deferrable, guaranteed background work — sync jobs, upload retries, anything that should eventually run even if the app is killed or the device reboots — and it's the direct successor to the older `JobScheduler`/`AlarmManager`/raw-`Service` patterns precisely because Android has steadily restricted what apps can do unattended in the background. It handles constraints (only run on Wi-Fi, only when charging), automatic retry with backoff, and survives process death, which is exactly why it's the right answer to "how would you reliably sync data even if the app gets killed," a question that shows up constantly in interviews for apps with any offline-first behavior.

## Dependency injection — Hilt and Dagger

Dependency injection means a class declares what it needs (its dependencies) without constructing them itself — they're supplied ("injected") from outside, usually by a framework. The motivating problem it solves: without DI, a `ViewModel` that needs a `Repository`, which needs an `ApiService` and a `Database`, ends up either constructing that whole chain itself (untestable — you can't substitute a fake repository in a unit test) or threading constructor parameters through five layers of code by hand.

**Dagger** is the underlying, fully general dependency-injection framework for Java/Kotlin, doing its wiring at compile time via annotation processing (so DI mistakes are caught at build time, not as a runtime crash) — but it's notoriously verbose, with a learning curve around `@Component`, `@Module`, `@Provides`, and `@Inject` that many teams found genuinely hard to onboard new engineers into.

**Hilt** is Google's opinionated layer on top of Dagger, purpose-built for Android, that removes most of the boilerplate by understanding Android's component lifecycle (Application, Activity, Fragment, ViewModel) natively:

```kotlin
@HiltAndroidApp
class MyApp : Application()

@Module
@InstallIn(SingletonComponent::class)
object NetworkModule {
    @Provides
    @Singleton
    fun provideApiService(): ApiService =
        Retrofit.Builder()
            .baseUrl("https://api.example.com/")
            .build()
            .create(ApiService::class.java)
}

@HiltViewModel
class ProfileViewModel @Inject constructor(
    private val repo: ProfileRepository
) : ViewModel() { /* ... */ }

@AndroidEntryPoint
class ProfileFragment : Fragment() {
    private val viewModel: ProfileViewModel by viewModels()
}
```

The interview-relevant comparison: Hilt isn't a replacement *framework*, it's Dagger with Android-specific scopes (`@Singleton`, `@ActivityScoped`, `@ViewModelScoped`) and components pre-wired, which is exactly why almost all new Android projects default to Hilt rather than raw Dagger now — you get Dagger's compile-time safety with a fraction of the manual component graph wiring. Knowing that Hilt sits *on top of* Dagger, rather than being an unrelated alternative, is the detail that separates a memorized one-liner ("Hilt is easier than Dagger") from an actual understanding of the relationship.

## How Android interviews differ from "I'll just grind a question bank"

Here's where most candidates' prep goes wrong, and it's worth being specific about each common approach and where it actually falls short — not as a knock on people who use them, but because being honest about tradeoffs is more useful than vague hype.

**A GeeksforGeeks-style "Top 100 Android Interview Questions" page.** These are genuinely useful as a checklist of *topics* — you'll see "explain the activity lifecycle" and "MVVM vs MVP" on basically every one, which is exactly why this guide covers the same ground. Where they fall short: they're built for skimming, so the answers are often a sentence or two, missing the "why," and they give you zero practice at the actual skill being tested in the interview — explaining a concept out loud, in your own words, while someone asks a follow-up you didn't anticipate. Reading "ViewModel survives configuration changes because Android retains the ViewModelStore" is not the same skill as saying it out loud, fluently, when someone just asked you "wait, why doesn't the Activity itself just survive instead?"

**LeetCode.** Excellent, close to mandatory, for the data-structures-and-algorithms portion of an Android interview at a product company — but it tests almost none of the lifecycle/architecture/Jetpack material that fills the *other* 60% of a typical Android round. A candidate who's LeetCode-strong and lifecycle-weak is a common, specific failure mode at mid-size product companies that run separate DSA and Android-specific rounds.

**A friend's WhatsApp-forwarded PDF of "Android Interview Questions"** — every developer has one of these, usually a few years stale (still asking about `AsyncTask`, which Google deprecated, or treating `LiveData` as the only modern answer with no mention of `StateFlow`). Useful as a quick refresher of topic *coverage*, risky as your only source of *correctness* for a fast-moving platform — Android's recommended patterns shift roughly every year or two (this very guide will look dated in a couple of years for the same reason the PDF you got from a senior in 2023 looks dated now).

**Generic ChatGPT prompting** ("give me Android interview questions") gets you broad, often-correct, occasionally subtly-wrong answers fast — but it's a static text exchange. It won't notice that you've quietly conflated `Activity` and `Application` context for the third time, won't push back when you give a vague non-answer, and won't simulate the actual pressure of a real interviewer's follow-up landing while you're mid-sentence.

**Following along with a YouTube Compose tutorial** builds real, valuable hands-on familiarity with syntax — genuinely useful, not a waste of time. But typing code while a video explains it is a fundamentally different skill from explaining your own reasoning, unprompted, to a stranger evaluating you in real time, with no pause button.

**Where Greenroom is different, and the honest tradeoff:** [Greenroom](/) runs spoken Android mock interviews — you talk through the lifecycle, MVVM, coroutines, or a memory-leak scenario out loud, the AI interviewer asks real follow-ups based on what you actually said (not a scripted list), and you get feedback on how clearly you explained it, not just whether the final answer was technically correct. The tradeoff worth being upfront about: it doesn't replace hands-on coding practice (still use Android Studio, still build things, still use LeetCode for DSA) or topic-coverage refreshers (still skim a checklist like this one) — it specifically targets the verbal, under-follow-up-pressure gap that question banks, static AI chat, and tutorials all structurally can't.

None of this is an argument to abandon the other methods — a realistic prep stack for most candidates is genuinely "all of the above, in proportion": a checklist (like this guide, or a GeeksforGeeks-style page) for topic coverage, hands-on building plus a Compose or Kotlin coroutines tutorial for muscle memory, LeetCode for the DSA round, and spoken mock practice for the part that's actually load-bearing in the interview room — your ability to think out loud, recover gracefully from a follow-up you didn't expect, and explain *why*, not just *what*. Treat the silent-reading methods as building the knowledge, and the spoken practice as the only thing that tests whether you can deploy that knowledge live, under mild social pressure, the way the real interview actually works.

## A worked example — explaining a memory leak fix out loud, the way you'd actually say it in an interview

It's worth seeing what a *good verbal answer* sounds like end to end, because the gap between "knows the fix" and "explains the fix well" is exactly what most prep misses.

**Interviewer:** "Tell me about a memory leak you've actually debugged."

**Weak answer:** "Yeah, I had a leak once with a fragment. I added LeakCanary and it found it. I fixed the binding thing." *(True, but gives the interviewer nothing to grab onto — no specifics, no reasoning, no signal of depth.)*

**Stronger answer:** "On a bottom-nav app I worked on, users navigating between the Home and Profile tabs repeatedly caused memory to climb — never crashed outright, but Profiler showed `Fragment` instance counts that should've dropped to zero after navigating away staying nonzero. I added LeakCanary, reproduced it by bouncing between tabs a dozen times, and the reference chain showed the old `FragmentProfileBinding` being held by a fragment instance that the back stack still had a reference to — I'd set up the binding in `onCreateView` but wasn't nulling it out in `onDestroyView`, so the destroyed view's whole hierarchy was being kept alive through that binding object. The fix was a one-line null-out in `onDestroyView`, but the part I actually want to highlight is *why* it leaked — the fragment instance legitimately needs to outlive its view across back-stack transactions, so the binding-to-view link has to be broken manually; it's not a bug in the framework, it's a contract the framework expects you to honor."

Notice what the stronger answer does: specific symptom (Profiler, instance counts), specific tool (LeakCanary, reference chain), specific root cause (binding not nulled), and — the part that actually gets offers — *why* it happened structurally, not just that it did. That's the difference practising out loud, with real follow-up pressure, builds that silent reading never will.

## Frequently asked questions

### What are the most common Android developer interview questions?

The most common topics are the activity and fragment lifecycle (almost always asked first), how configuration changes like rotation are handled, MVVM vs MVP vs MVC and why MVVM is standard, ViewModel/LiveData/StateFlow and why ViewModel survives rotation, Jetpack Compose vs XML views and recomposition, Kotlin coroutines and structured concurrency, memory leaks (especially Activity context leaks and the ViewBinding-in-onDestroyView leak), RecyclerView and DiffUtil, and dependency injection with Hilt/Dagger.

### Why does a ViewModel survive a configuration change like screen rotation?

A ViewModel survives rotation because the ViewModelStore that holds it is retained by the Activity/FragmentManager machinery across the destroy-and-recreate cycle, and the new activity instance is handed back the same ViewModelStore rather than a fresh one. The ViewModel is only actually destroyed when the activity finishes for good, not when it's merely recreated for a configuration change.

### Should I learn Jetpack Compose or focus on XML views for interviews?

Learn both if you can, but prioritize Compose for new interview prep since most teams building new screens default to it in 2026 and ask about recomposition and state hoisting. Don't skip XML entirely — a large share of real, revenue-generating Android codebases are still XML-based, interop between the two is common, and claiming zero XML experience can read as a red flag for a role described as senior or requiring several years of experience.

### What's the difference between LiveData and StateFlow, and which should I use?

LiveData is lifecycle-aware out of the box and automatically stops delivering updates to destroyed observers; StateFlow always has a current value and composes naturally with the rest of the Kotlin coroutines and Flow ecosystem but needs to be collected with lifecycle awareness manually, typically via repeatOnLifecycle. StateFlow is the more idiomatic choice for new, coroutines-first or Compose-first code; LiveData remains a perfectly fine choice in mature XML-based codebases where rewriting working code for its own sake isn't worth the churn.

### What causes most Android memory leaks?

The most common causes are holding an Activity context in a long-lived object like a singleton or static field instead of an Application context, forgetting to unregister listeners or cancel coroutines tied to a destroyed lifecycle owner, and not nulling out a ViewBinding reference in a Fragment's onDestroyView even though the fragment instance itself is still retained by the back stack. LeakCanary and the Android Studio Profiler's heap dump and instance-count views are the standard tools for finding these in practice.

### How is Hilt different from Dagger?

Hilt is not a separate framework from Dagger — it's an opinionated layer built on top of Dagger, purpose-built for Android, that pre-wires Android-specific scopes and components (Application, Activity, Fragment, ViewModel) so you get Dagger's compile-time-safe dependency injection with far less manual component-graph boilerplate. Almost all new Android projects default to Hilt rather than raw Dagger for exactly this reason.

### How should I practise for the verbal part of an Android interview?

Practise explaining concepts out loud, not just recognizing them on a question-bank page — interviewers are listening for whether you understand why something works, not whether you can recite a definition, and that's a different skill from silent reading. A spoken mock interview that asks realistic follow-up questions on topics like the lifecycle, MVVM, or a real memory leak you've fixed is the closest practice to the actual interview; pair it with hands-on coding in Android Studio and a quick skim of a topic checklist for coverage, since no single method covers everything alone.

Android interviews reward developers who can explain the platform's "why," out loud, under real follow-up questions — not just recite the lifecycle diagram. [Greenroom](/) runs spoken Android mock interviews with follow-ups on lifecycle, architecture, coroutines, and memory-leak scenarios, with feedback on how clearly you explained each answer. Pair it with our [Kotlin interview questions](/blog/kotlin-interview-questions) guide, our [iOS developer interview questions](/blog/ios-developer-interview-questions) guide if you're comparing mobile platforms, our [Java interview questions](/blog/java-interview-questions) guide for the JVM fundamentals underneath Kotlin, [system design interviews: what they test](/blog/system-design-interviews-what-they-test) for senior Android system-design rounds, and [coding interview communication tips](/blog/coding-interview-communication-tips) for the DSA half of the loop. Free to start.
