Senior Android developer interviews test whether you can ship reliable mobile features under real constraints—process death, flaky networks, battery limits, and large codebases—not whether you can name every Activity callback from memory. Interviewers at the senior level want trade-offs: why MVVM over MVI for your team, how you structure offline sync, and what you would cut from scope when the deadline is fixed.
Below are 40 senior Android developer interview questions grouped by Kotlin, lifecycle, Compose, coroutines, architecture, offline-first data, performance, testing, and system design. Open each answer after you try the question yourself. For the iOS side of mobile prep, see iOS developer interview questions. For JVM and concurrency fundamentals that underpin Kotlin, see Java interview questions (part 1) and part 2.
Interview context and how to prepare
What extra bar do senior Android interviews add?
Mid-level screens check whether you can implement features. Senior screens check whether you can own them across teams and production edge cases.
| Area | Mid-level signal | Senior signal |
|---|---|---|
| UI | Build screens, handle rotation | Compose performance, state ownership, accessibility |
| Data | Call APIs, cache in Room | Offline-first, sync conflicts, single source of truth |
| Concurrency | Use viewModelScope.launch |
Structured concurrency, Flow backpressure, testing |
| Architecture | Explain MVVM layers | Justify MVVM vs MVI, module boundaries, migration |
| Delivery | Fix bugs | Incidents, rollout strategy, mentoring, tech debt trade-offs |
Common senior-only rounds:
- System design — feed, chat, checkout, or sync architecture on a whiteboard
- Code review — spot leaks, wrong scope, or fragile state
- Behavioral — STAR stories with metrics (crash rate, retention, latency)
What is a typical senior Android interview loop?
| Round | Duration | Focus |
|---|---|---|
| Recruiter / hiring manager | 30 min | Background, stack, leadership scope |
| Kotlin / platform deep dive | 45–60 min | Language, lifecycle, coroutines, Compose |
| Architecture & code review | 45–60 min | MVVM/MVI, DI, modularization, past projects |
| Live coding | 45–90 min | Feature slice, pagination, or refactor exercise |
| System design | 45–60 min | Offline-first, sync, notifications, scale |
| Behavioral | 30–45 min | Ownership, conflict, incidents, mentoring |
Some companies merge platform and architecture into one pair programming session on a take-home or shared repo.
What to bring: Two shipped apps you can whiteboard—data flow, testing strategy, and one hard bug or incident you fixed.
What is a realistic 4–6 week prep plan?
| Week | Focus | Output |
|---|---|---|
| 1 | Kotlin idioms, null safety, sealed classes, extensions | Explain 10 language features with examples |
| 2 | Lifecycle, process death, ViewModel, SavedState | Trace rotation + kill + restore aloud |
| 3 | Compose state, side effects, stability | Build one small screen with loading/error |
| 4 | Coroutines, Flow, Room, repository pattern | Diagram UI → VM → repo → local/remote |
| 5 | System design: offline-first, pagination, push | Whiteboard one feed or sync design |
Daily habit: Pick one scenario—"user submits form on airplane mode"—and explain UI, domain, data, and recovery in under three minutes.
Do you need Jetpack Compose for senior roles in 2026?
Most greenfield Android teams expect Compose fluency or a credible migration plan. Legacy View/XML still exists in large apps, but interviewers use Compose questions to test modern state management.
| Signal | What interviewers hear |
|---|---|
| Compose-first team | State hoisting, side effects, stability, testing |
| View-heavy codebase | Migration strategy, interop, when not to rewrite |
| Weak answer | "I only know XML" with no learning plan |
You do not need to be a Compose library author—but you should explain recomposition, remember vs rememberSaveable, and where state lives.
How much Java do senior Android developers need?
Kotlin is the interview default for new code. Seniors still encounter Java in older modules, SDK samples, and stack traces.
| Topic | Kotlin focus | Java tie-in |
|---|---|---|
| Nullability | String?, smart casts |
Java platform types / annotations |
| Concurrency | coroutines / Flow |
threads, executors, locks |
| Collections | Kotlin APIs | Java collection interoperability |
| Legacy code | Kotlin-first new modules | reading and debugging Java modules |
When comparing coroutines with raw threads, explain that suspension can free the underlying thread while waiting for I/O, allowing many concurrent operations without one blocked thread per task. Kotlin implements suspend functions through compiler-generated state-machine machinery.
Kotlin language depth
Explain Kotlin null safety—how is it different from Java?
What interviewers are testing: Whether you design nullability at Java/API/storage boundaries rather than sprinkling !!.
Kotlin separates nullable and non-null types at compile time.
| Type | Meaning |
|---|---|
String |
Cannot hold null |
String? |
May hold null |
?. |
Safe call — skips if null |
?: |
Elvis — default if null |
!! |
Assert non-null — throws NPE if wrong |
fun length(name: String?): Int =
name?.length ?: 0Senior follow-up: Prefer explicit null handling at boundaries (API, DB, intents). Avoid !! in production paths—use early return, requireNotNull, or checkNotNull with messages.
A strong answer is:
Kotlin encodes nullability in the type system. I use safe calls and Elvis at boundaries and treat !! as a code smell except in tests or proven invariants.
When do you use data class vs sealed class?
What interviewers are testing: Whether you distinguish value records from closed state/result hierarchies.
Both model structured data; the choice is about closed hierarchies vs flat records.
| Construct | Best for |
|---|---|
data class |
Immutable DTOs, UI state snapshots, API models |
sealed class / sealed interface |
Finite result types, UI events, navigation destinations |
sealed class UiState {
data object Loading : UiState()
data class Success(val items: List<Item>) : UiState()
data class Error(val message: String) : UiState()
}Compose tie-in: Sealed hierarchies make when exhaustive—compiler forces you to handle every branch.
A strong answer is:
data class for product-shaped records; sealed class when the set of outcomes is fixed and I want exhaustive when without an else branch.
What are inline functions and reified type parameters?
What interviewers are testing: Whether you understand what inlining changes and why erased generic type information can become available to reified parameters.
inline asks the Kotlin compiler to inline the function body and eligible lambdas at call sites, which can reduce higher-order-function overhead and enables reified type parameters.
inline fun <reified T> Gson.fromJson(json: String): T =
fromJson(json, T::class.java)Interview use cases:
- Type-safe intent extras (
inline fun <reified T> Bundle.getParcelable) - DSL builders
- Performance-sensitive higher-order functions
Caution: Large inlined functions inflate bytecode—not every higher-order function should be inline.
A strong answer is:
inlineplusreifiedlets me accessT::classat runtime without passingClassobjects—common for type-safe parsing and Android parcelable helpers.
Extension functions vs utility classes—when do you use each?
What interviewers are testing: Whether you use extensions for cohesive receiver-specific behavior without pretending they add polymorphic members.
Extensions add behavior to existing types without inheritance—idiomatic Kotlin for Android APIs.
| Approach | When |
|---|---|
| Extension | Thin wrappers on framework types (Context, String, Flow) |
| Top-level function | Pure helpers with no receiver |
| Class with static utils | Rare in Kotlin—prefer extensions or objects |
fun String.isValidEmail(): Boolean =
Patterns.EMAIL_ADDRESS.matcher(this).matches()Keep extensions discoverable—group in StringExt.kt, avoid dumping unrelated helpers on Context.
A strong answer is:
Extensions keep call sites readable for Android framework types. I avoid god-object extension files and prefer cohesive *Ext.kt modules.
Activity, Fragment, and process lifecycle
Walk through Activity lifecycle—and what matters for seniors?
What interviewers are testing: Whether you connect lifecycle callbacks to state survival and cleanup—not recite every callback. Interviewers care less about reciting every callback and more about state survival and resource cleanup.
| Callback | Senior talking point |
|---|---|
onCreate |
Initialize the new Activity instance; restore saved UI state where appropriate |
onStart / onStop |
Visible but may not be interactive |
onResume / onPause |
Foreground interaction; pause heavy work |
onDestroy |
Called when the Activity is finishing or being destroyed for recreation; do not rely on it for process-death cleanup because the system may kill the process without calling it |
Configuration change: Without ViewModel or rememberSaveable, rotation recreates the Activity—dropping in-memory state.
Process death: OS may kill your process under memory pressure. Persistent app data (Room, DataStore, files) survives; small restorable UI state uses SavedStateHandle or rememberSaveable; plain Activity fields do not.
A strong answer is:
I focus on what survives rotation vs process death. ViewModel survives configuration changes but not process death. After system-initiated process death, small restorable UI state can come from saved-state APIs, while durable application data comes from Room/DataStore/files.
What is the role of ViewModel—and what should NOT go in it?
What interviewers are testing: Whether you understand screen-level state ownership and lifetime, not treat ViewModel as a place for all logic.
A ViewModel is retained while its ViewModelStoreOwner scope survives configuration changes. The enclosing owner—Activity, Fragment, navigation destination/graph, or other owner—determines lifetime.
Belongs in ViewModel:
- UI state (
StateFlow,UiState) - Calls to repositories / use cases
- Coroutine work in
viewModelScope
Does not belong:
Activity,Fragment,Context, orResourcesreferences for convenience — Android architecture guidance discourages passing UI/context types into ViewModel and even discouragesAndroidViewModelfor new code- Navigation side effects without a clear pattern (prefer representing outcomes in UI state)
- Android framework classes that tie to lifecycle shorter than the VM
class FeedViewModel(
private val repo: FeedRepository
) : ViewModel() {
private val _state = MutableStateFlow<UiState>(UiState.Loading)
val state: StateFlow<UiState> = _state.asStateFlow()
fun load() = viewModelScope.launch {
_state.value = UiState.Success(repo.getFeed())
}
}A strong answer is:
ViewModel holds UI state and orchestrates use cases across rotation. I keep Android UI types out and survive process death with SavedStateHandle or Room—not memory-only fields.
What is process death and how do you design for it?
What interviewers are testing: Whether you design for process death with SavedStateHandle and persistent storage—not in-memory-only state. Android may kill your entire process when memory is low. When the user returns, the app restarts—Activities and ViewModels are recreated.
| Survives process death? | Mechanism |
|---|---|
| Yes | Room, DataStore, files |
| Small restorable UI state | SavedStateHandle, rememberSaveable |
| No | In-memory singletons, static caches, plain remember state |
Senior pattern: Single source of truth in Room; UI observes Flow from DB. Network refresh updates DB—not a fragile in-memory list.
Test: For recreation testing, use "Don't keep activities" or explicit Activity recreation. For real process-death restoration, use tooling/tests that kill the app process while preserving the task/saved state, then relaunch/return to the task. Do not treat Activity destruction alone as process death.
A strong answer is:
Process death wipes memory. I persist user-visible state in Room or DataStore and treat in-memory caches as optional performance layers only.
Fragment vs single-Activity Compose—what do seniors need to know?
What interviewers are testing: Whether you can articulate Fragment/XML vs single-Activity Compose trade-offs and migration without duplicating business logic.
Modern apps often use single-Activity + Compose Navigation. Legacy apps use multi-Fragment navigation.
| Approach | Trade-off |
|---|---|
| Fragments + XML | Mature back stack, lots of existing code |
| Single Activity + Compose | Simpler hosting, declarative UI, shared transitions |
| Hybrid | AndroidViewBinding / ComposeView interop during migration |
Interviewers want a migration story: new features in Compose, shared navigation graph, avoid duplicating business logic in both View and Compose layers.
A strong answer is:
I know Fragment back-stack rules for legacy code. For greenfield I prefer single-Activity Compose with Navigation-Compose and keep business logic in ViewModels and repositories either way.
Jetpack Compose
What are the three phases of Jetpack Compose?
What interviewers are testing: Whether you can connect composition, layout, and draw invalidation to performance diagnosis. Compose rendering is organized into three main phases: composition, layout, and draw. Depending on what state changes and where it is read, Compose may need to rerun only the affected phase(s).
| Phase | What happens |
|---|---|
| Composition | Build or update the UI tree (run @Composable functions) |
| Layout | Measure and place nodes |
| Draw | Paint pixels |
Performance angle: State can invalidate composition, layout, or draw depending on where it is read. Reduce work in the phase that is actually hot; for rapidly changing visual values, deferring reads to layout/draw can sometimes avoid unnecessary recomposition.
A strong answer is:
Composition decides structure, layout measures/places it, and draw renders pixels. I profile which phase is doing excess work rather than assuming recomposition is always the bottleneck.
What triggers recomposition—and how do you reduce it?
What interviewers are testing: Whether you understand state-read scopes and stability, not simply "avoid recomposition." Recomposition runs when state read during composition changes.
Reduction tactics:
| Technique | Purpose |
|---|---|
| Stable / immutable models | Compiler can skip unchanged composables |
remember |
Retain a value across recompositions while that composition remains alive |
derivedStateOf |
Expose derived state and avoid downstream recomposition when the derived result itself has not changed |
| Lambda modifiers | Defer state reads to layout/draw when possible |
| Keys in lists | Correct identity for LazyColumn items |
val listState = rememberLazyListState()
val showFab by remember {
derivedStateOf { listState.firstVisibleItemIndex > 0 }
}A strong answer is:
Recomposition follows state reads. I stabilize models, hoist state, use
derivedStateOffor derived UI flags, and profile with Layout Inspector when lists jank.
Explain state hoisting with a concrete example.
What interviewers are testing: Whether you place state at the right owner while keeping reusable composables stateless where useful.
State hoisting moves state up to the lowest common owner so child composables stay stateless and reusable.
@Composable
fun SearchScreen(viewModel: SearchViewModel = hiltViewModel()) {
val query by viewModel.query.collectAsStateWithLifecycle()
SearchContent(
query = query,
onQueryChange = viewModel::onQueryChange
)
}
@Composable
fun SearchContent(
query: String,
onQueryChange: (String) -> Unit
) {
TextField(value = query, onValueChange = onQueryChange)
}Rule: State flows down as parameters; events flow up as callbacks.
A strong answer is:
I keep leaf composables stateless—state lives in ViewModel or parent, children receive value plus event callbacks. That improves preview and testability.
remember vs rememberSaveable—when do you use each?
What interviewers are testing: Whether you choose the right state survival API for composition, recreation, and process death.
| API | Survives |
|---|---|
remember |
Recomposition while that composition remains alive |
rememberSaveable |
Recomposition + activity recreation + supported system-initiated process recreation |
ViewModel |
Configuration changes, but not process death by itself |
SavedStateHandle |
Small state needed to restore after process death |
Use rememberSaveable for small UI state—scroll position, expanded flags, draft text—not large lists (use Room).
Current Compose also provides rememberSerializable, which has similar saved-state lifetime semantics to rememberSaveable but uses Kotlinx Serialization for serializable custom types. Use it when that serialization model fits; keep saved state small either way.
Parcelable / list savers exist for custom types—mind Bundle size limits.
A strong answer is:
remembersurvives recomposition only while that composition remains alive.rememberSaveableandSavedStateHandlehandle small UI state across recreation/process death; large data belongs in Room or DataStore.
LaunchedEffect vs DisposableEffect vs SideEffect?
What interviewers are testing: Whether you select an Effect API based on lifecycle and cleanup semantics. Compose side-effect APIs tie work to the composition lifecycle:
| API | Use when |
|---|---|
LaunchedEffect(key) |
Suspend work tied to keys—animation, snackbar, lifecycle-tied collection when collection belongs in the composable |
DisposableEffect(key) |
Setup + cleanup on leave (listeners, subscriptions) |
SideEffect |
Publish Compose state to non-Compose code after every successful recomposition |
rememberUpdatedState |
Capture latest callback in long-lived effect without restarting |
LaunchedEffect(snackbarMessage) {
snackbarHostState.showSnackbar(snackbarMessage)
}
DisposableEffect(lifecycleOwner) {
val observer = LifecycleEventObserver { _, event -> /* ... */ }
lifecycleOwner.lifecycle.addObserver(observer)
onDispose { lifecycleOwner.lifecycle.removeObserver(observer) }
}Do not use LaunchedEffect merely to move ordinary screen initialization into composition when the ViewModel/data layer can own it directly.
A strong answer is:
LaunchedEffectfor coroutine work keyed to composition;DisposableEffectwhen I must unregister on dispose;rememberUpdatedStateto avoid stale lambdas in effects.
How do you migrate from Views to Compose incrementally?
What interviewers are testing: Whether you migrate Views to Compose incrementally with shared data layers—not a big-bang rewrite.
Incremental migration beats big-bang rewrite.
| Strategy | When |
|---|---|
| New screens in Compose | Low-risk features first |
ComposeView in Fragment/Activity |
Embed one composable in XML host |
AndroidView in Compose |
Wrap legacy custom View |
| Shared ViewModel + repository | One business layer for both UIs |
Senior point: Measure crash and ANR rates per release; feature-flag Compose screens; keep navigation consistent.
A strong answer is:
I migrate screen by screen with shared ViewModels and data layers. Interop bridges let legacy and Compose coexist until the back stack is ready to simplify.
Coroutines and Flow
What is structured concurrency—and why do seniors need it?
What interviewers are testing: Whether child work has bounded lifetime, cancellation propagation, and predictable failure behavior. Structured concurrency keeps related child coroutines inside an explicit scope so their lifetime, cancellation, and failure are tied to that scope—not orphaned work after the user leaves the screen.
viewModelScope.launch {
val user = async { repo.loadUser() }
val feed = async { repo.loadFeed() }
_state.value = UiState.Ready(user.await(), feed.await())
} // cancelled automatically when ViewModel clearsAnti-pattern: GlobalScope.launch for UI work—survives the screen, leaks, hard to test. Contrast with detached/unstructured work that outlives its owner.
A strong answer is:
Structured concurrency ties async work to a lifecycle scope. I use
viewModelScopeorlifecycleScope—notGlobalScope—for UI-driven coroutines.
Explain coroutine Dispatchers—Main, IO, Default.
What interviewers are testing: Whether you distinguish blocking I/O, CPU work, and main-safe suspend APIs.
| Dispatcher | Use for |
|---|---|
Dispatchers.Main |
UI updates, fast work on main thread |
Dispatchers.IO |
Blocking I/O APIs — disk, blocking database drivers, legacy blocking network code |
Dispatchers.Default |
CPU-heavy work—parsing, sorting, crypto |
Dispatchers.IO is for blocking I/O APIs. Many modern Android clients already expose main-safe suspend functions — for example Retrofit suspend calls and Room suspend APIs — so callers should not blindly wrap every network/database call in withContext(IO) when the API is already main-safe.
suspend fun loadFeed(): List<Post> = withContext(Dispatchers.IO) {
blockingFileApi.readPosts()
}Senior nuance: Suspend does not automatically mean background — only dispatcher choice or a main-safe library implementation moves work off Main.
A strong answer is:
Main for UI, IO for known blocking I/O, Default for CPU work. I don't wrap every suspend API in IO—the data-layer API should be main-safe, and I move only genuinely blocking work.
launch vs async—when do you use each?
What interviewers are testing: Whether you understand parallel results, failure propagation, and supervisorScope trade-offs.
| Builder | Returns | Use when |
|---|---|---|
launch |
Job — fire-and-forget side effect |
Send analytics, trigger sync |
async |
Deferred<T> — await a result |
Parallel requests you need to combine |
coroutineScope {
val profile = async { repo.profile() }
val settings = async { repo.settings() }
ProfileScreen(profile.await(), settings.await())
}Exception handling: async stores a result in Deferred, and await() returns it or rethrows its failure. In a normal coroutineScope, failure of a child also cancels the scope and siblings; use supervisorScope when sibling failures should be isolated.
A strong answer is:
launchfor side effects;asyncwhen I need parallel results and will awaitDeferred. I handle failures atawaitand considersupervisorScopefor partial success.
Which Flow operators do you use most in production?
What interviewers are testing: Whether you understand cancellation/switching semantics, especially for search and rapidly changing input. Common operators seniors should explain, not only name:
| Operator | Purpose |
|---|---|
map / filter |
Transform streams |
flatMapLatest |
Switch search query—cancel stale requests |
distinctUntilChanged |
Skip duplicate UI emissions |
catch |
Handle upstream errors |
flowOn(Dispatchers.IO) |
Run upstream on IO |
searchQuery
.debounce(300)
.distinctUntilChanged()
.flatMapLatest { query ->
repo.search(query)
}Handle errors without converting a failed request into a valid empty result—for example expose a typed success/error state from the repository or ViewModel.
A strong answer is:
debounceplusflatMapLatestfor search,distinctUntilChangedfor UI—I explain backpressure and cancellation, and handle errors in the repository or ViewModel without masking failures as empty results.
How do you test coroutines and Flow?
What interviewers are testing: Whether you can make async code deterministic through injected dispatchers and virtual time.
| Tool | Use |
|---|---|
runTest |
Virtual time, controlled dispatchers |
TestDispatcher |
Replace Dispatchers.Main in tests |
turbine (library) |
Assert Flow emissions in order |
@Test
fun loadSuccess() = runTest {
val vm = FeedViewModel(FakeRepo())
vm.load()
assertEquals(UiState.Success, vm.state.value)
}Senior point: Prefer keeping blocking-dispatcher decisions in the data layer rather than making ViewModels switch to IO. Inject dispatchers where code actually owns blocking/CPU work and needs deterministic tests.
A strong answer is:
I use
runTestwith injected dispatchers and fakes for repositories. For Flow I assert emissions with turbine or collect in a controlled scope.
Architecture and modularization
MVVM vs MVI—which do you pick and why?
What interviewers are testing: Whether you choose MVVM or MVI based on screen complexity and team conventions—not pattern dogma.
| Pattern | State model | Best when |
|---|---|---|
| MVVM | Multiple flows or single UiState |
Team knows Jetpack; flexible screens |
| MVI | Single immutable state + sealed intents | Complex state machines, strict unidirectional flow |
MVVM (common Jetpack shape):
// ViewModel exposes StateFlow<UiState>
// View calls fun onIntent(action: UserAction)MVI: UserIntent → reducer → UiState → UI (loop is explicit).
A strong answer is:
I default to MVVM with a single UiState data class for clarity. I choose MVI when the screen is a state machine with many transitions and I want intents logged for debugging.
How do you apply Clean Architecture on Android?
What interviewers are testing: Whether architecture boundaries reduce coupling without adding ceremony for its own sake. Typical layers:
| Layer | Responsibility |
|---|---|
| UI | Compose, ViewModel, navigation |
| Domain | Use cases, pure Kotlin rules (no Android imports) |
| Data | Repository impl, Room, Retrofit, DataStore |
Dependency rule: In a strict Clean Architecture variant, domain-facing abstractions may be defined inward and implemented by data. In pragmatic Android architectures, repository interfaces can live at the boundary that best preserves module ownership and testability.
Pragmatic senior take: Not every app needs a UseCase class per action—avoid ceremony until the team benefits from test seams.
See design patterns for shared pattern vocabulary (repository, factory, observer).
A strong answer is:
UI observes ViewModels; ViewModels call use cases or repositories; data layer implements repos against Room and network. I keep domain Android-free and avoid use-case explosion on small teams.
What belongs in a Repository?
What interviewers are testing: Whether the repository owns data-source coordination and consistency, not merely wraps Retrofit. Repository is the single API for data—UI does not care if data came from cache or network.
Responsibilities:
- Merge remote + local (Room as source of truth)
- Expose
Flowfor reactive UI - Handle retry, error mapping, offline
class UserRepository @Inject constructor(
private val api: UserApi,
private val dao: UserDao
) {
fun observeUser(id: String): Flow<User?> =
dao.observeUser(id).map { entity -> entity?.toDomain() }
suspend fun refresh(id: String) {
val remote = api.getUser(id)
dao.upsert(remote.toEntity())
}
}A strong answer is:
Repository hides data sources and exposes one observable API. UI collects
Flow; refresh writes through to Room so offline still works—the ViewModel decides not-found vs empty UI.
How do you modularize a large Android app?
What interviewers are testing: Whether module boundaries improve ownership/build isolation/API boundaries without producing dependency spaghetti. Modularization improves build time, enforces boundaries, and enables feature teams.
| Module type | Contains |
|---|---|
:app |
DI graph, navigation host, minimal glue |
:feature:* |
Screen UI + ViewModel |
:core:ui |
Design system, shared composables |
:core:data |
Network, DB, repositories |
:core:domain |
Use cases, models |
Rules: Prefer clear, acyclic feature dependencies. Where features need to collaborate, depend on narrow contracts/navigation APIs rather than reaching into another feature's internals.
A strong answer is:
I split by feature and core layers, keep dependencies pointing inward, and avoid feature-to-feature imports. Navigation contracts decouple teams.
Hilt basics—what problem does DI solve on Android?
What interviewers are testing: Whether you understand lifetime and dependency ownership, not annotation memorization. Dependency injection supplies dependencies from the outside—testable, swappable, lifecycle-aware.
| Hilt component | Lifetime / typical use |
|---|---|
SingletonComponent |
Application lifetime |
ActivityRetainedComponent |
Retained across Activity configuration changes |
ViewModelComponent |
One ViewModel lifetime; dependencies scoped with @ViewModelScoped |
@HiltViewModel
class FeedViewModel @Inject constructor(
private val repo: FeedRepository
) : ViewModel()Interview follow-up: Manual DI works for small apps; Hilt/Koin scale when graphs grow. Prefer constructor injection over field injection.
A strong answer is:
DI makes dependencies explicit and testable. I use Hilt for ViewModel and singleton bindings—fake repos in unit tests without Robolectric.
Networking, persistence, and offline-first
Why is Room often the single source of truth?
What interviewers are testing: Whether UI reads from a durable authoritative model while refresh/sync updates it. Many offline-first designs make a durable local data source such as Room the canonical read source, while network/sync updates it.
| Pattern | Behavior |
|---|---|
| UI observes | Flow from Room |
| Network success | Upsert into Room |
| Network failure | UI still shows last cached data + error affordance |
Benefits: Survives process death, rotation, and airplane mode without custom cache glue.
A strong answer is:
Room gives durable observable state. Network is an update mechanism, not the primary read path—users still see data when offline.
How would you design offline sync with conflict resolution?
What interviewers are testing: Whether you design durable pending writes, idempotency, retries, and conflict semantics. Senior system-design favorite. Outline:
- Local write — optimistic UI, queue mutation in
outboxtable - Background sync — WorkManager worker drains outbox when online
- Conflict policy — last-write-wins, server version, or merge per field
- Idempotency — client-generated IDs or request keys
| Component | Role |
|---|---|
| Room | Entities + outbox + sync metadata (updatedAt, syncState) |
| WorkManager | Durable deferred sync with constraints and retry/backoff |
| API | Server version/revision, ETags/conditional writes, or domain-specific conflict metadata |
Clarify with interviewer: Chat vs catalog vs financial data need different conflict rules.
A strong answer is:
I persist locally first, sync with WorkManager, and define conflict rules per entity type. I mention idempotency and what the user sees when sync fails mid-flight.
How do you handle API errors end-to-end?
What interviewers are testing: Whether errors are translated into domain/UI decisions without leaking transport implementation details.
Map layers cleanly—do not leak HTTP codes into Compose.
| Layer | Responsibility |
|---|---|
| Retrofit | Typed responses, interceptors |
| Repository | Map to Result or domain errors |
| ViewModel | UiState.Error with user message + retry |
| UI | Snackbar, inline error, pull-to-refresh |
sealed class NetworkResult<out T> {
data class Success<T>(val data: T) : NetworkResult<T>()
data class HttpError(val code: Int, val body: String?) : NetworkResult<Nothing>()
data object Offline : NetworkResult<Nothing>()
}A strong answer is:
I map transport errors to domain results in the repository and expose sealed UI state. Users get actionable messages and retry—not raw 502 text.
How do you implement pagination in a feed?
What interviewers are testing: Whether you understand paging keys, invalidation, refresh/append failure, and local/remote coordination.
| Approach | When |
|---|---|
| Paging 3 library | Standard RecyclerView/Compose lists—keys, placeholders, retry |
| Cursor / keyset API | Large feeds, stable ordering |
| Offset | Simple APIs; can become expensive or inconsistent under large/changing datasets |
Compose: PagingData + LazyColumn + collectAsLazyPagingItems().
Senior points: Duplicate keys break diffing; handle refresh vs append; empty and error states in LoadState.
A strong answer is:
I use Paging 3 with keyset APIs when possible. I explain placeholder behavior, retry, and how refresh invalidates the cache without duplicating items.
Performance, security, and quality
How do you diagnose ANRs and UI jank?
What interviewers are testing: Whether you distinguish ANR, jank, and startup bottlenecks before applying fixes.
| Symptom | Tool / action |
|---|---|
| ANR | Main thread blocked—check Play Console traces, StrictMode |
| Jank | Perfetto and Android Studio system traces/profilers |
| Compose | Layout Inspector recomposition counts |
ANR causes: Blocked main thread, locks, binder/I/O, long callbacks.
Jank causes: Expensive render/layout/draw, bitmap work, large lists.
Startup: Baseline Profiles, startup tracing.
A strong answer is:
I profile before guessing—Main thread stacks for ANR, recomposition counts for Compose jank. Fixes are moving blocking work off Main and stabilizing list state.
What causes memory leaks on Android—and how do you prevent them?
What interviewers are testing: Whether you can spot long-lived references retaining short-lived UI objects.
| Cause | Prevention |
|---|---|
| Static reference to Activity | Avoid; use Application context carefully |
| Listener not removed | Unregister at the matching lifecycle boundary (onDestroyView, onStop, onDestroy, or DisposableEffect depending on ownership) |
| Long-lived coroutine holding View | Structured concurrency, no Activity in coroutine |
| Mis-scoped singleton | Hilt scopes match lifetime |
Tool: LeakCanary in debug builds; heap dumps for stubborn cases.
A strong answer is:
Leaks are usually long-lived references to short-lived UI. I match scope to lifecycle, remove listeners, and use LeakCanary in debug.
What security topics do senior Android interviews cover?
What interviewers are testing: Whether you can identify mobile-specific trust boundaries and storage/network/component risks.
| Topic | Practice |
|---|---|
| Network | TLS, certificate pinning (when justified) |
| Sensitive keys/credentials | Minimize storage; use Android Keystore-backed cryptographic keys where appropriate and store only the minimum sensitive material required |
| WebView | Disable risky JS bridges; validate URLs |
| Exported components | Minimize exported Activities/Services |
| Secrets | Do not embed privileged server secrets in the APK or deliver them to the client through Remote Config. Keep privileged secrets server-side; use short-lived credentials, backend mediation, and attestation where applicable |
Encryption-at-rest design depends on the threat model; do not assume a deprecated convenience wrapper makes secrets safe. EncryptedSharedPreferences and EncryptedFile from androidx.security:security-crypto are deprecated.
Proguard/R8: Obfuscation is not encryption—assume reverse engineering.
A strong answer is:
I minimize sensitive client storage, use Keystore where needed, avoid hard-coded secrets, minimize exported surfaces, and treat R8 as obfuscation—not a vault.
Testing and delivery
What is your Android testing strategy?
What interviewers are testing: Whether you place tests where they provide fast confidence with minimal implementation coupling.
| Layer | Tools | What to test |
|---|---|---|
| Unit | JUnit, MockK, coroutines test | ViewModel, use cases, mappers |
| Integration | Room in-memory, fake servers | Repository, DAO queries |
| UI | Compose UI tests, Espresso | Critical flows, accessibility |
Senior bar: Tests prove behavior, not implementation details—avoid asserting private methods.
CI: run fast unit/static checks on every PR; select critical instrumentation tests per PR and parallelize/shard broader device suites according to CI cost and risk.
A strong answer is:
Heavy unit coverage on ViewModels and repositories; fewer UI tests on money paths. I inject fakes and use
runTestfor coroutines.
What CI/CD steps matter for Android teams?
What interviewers are testing: Whether the pipeline manages quality, signing, artifact promotion, and staged release risk. Typical pipeline:
- Lint (Android Lint, detekt, ktlint)
- Unit tests
- Assemble debug/release
- Instrumentation (optional per PR)
- Sign & distribute (Firebase App Distribution, Play Internal)
Versioning: keep versionCode monotonically increasing for Play distribution; versionName can follow semantic/product versioning if the team chooses.
See Git interview questions for branch and review practices that pair with mobile CI.
A strong answer is:
Every PR runs lint and unit tests; release builds bump
versionCodeand go through staged rollout. I tie Git flow to what CI actually gates.
Mobile system design scenarios
Design an offline-first news feed.
What interviewers are testing: Whether you design offline-first feeds with durable reads, sync ordering, retry, and battery-aware refresh. Structure your answer in layers:
Requirements to clarify: Read offline? Personalization? Media attachments? Stale tolerance?
| Layer | Choice |
|---|---|
| UI | Compose + Paging 3 + pull-to-refresh |
| State | ViewModel + UiState |
| Data | Room pages + RemoteMediator |
| Network | Cursor/keyset pagination; conditional requests/version metadata where supported |
| Sync | WorkManager prefetch + retry |
| Images | Coil/Glide with disk cache |
Edge cases: Airplane mode mid-pagination, process death mid-scroll, duplicate pages on retry.
A strong answer is:
Room plus Paging with RemoteMediator, network as refresh path, WorkManager for background sync, and explicit stale-while-revalidate UX.
Design a real-time chat feature on Android.
What interviewers are testing: Whether you balance real-time delivery, offline queues, ordering, and battery limits on Android. Clarify: One-to-one vs group, delivery receipts, offline queue, end-to-end encryption scope.
| Concern | Approach |
|---|---|
| Transport | WebSocket or SSE for live; REST for history |
| Local | Room messages table, outbox for sends |
| Ordering | Server sequence or hybrid logical clock |
| Push | FCM for wake; sync on open |
| UI | Paging history upward, optimistic send |
Battery: Batch heartbeats; use WorkManager for non-urgent sync. Do not rely on a continuously connected socket for background delivery—Android background limits and battery constraints make push + durable resync the safer model.
A strong answer is:
Optimistic UI with outbox, WebSocket when foreground, FCM plus sync on resume, Room as source of truth for history and pending sends.
How do push notifications fit into app architecture?
What interviewers are testing: Whether you treat push as a wake signal and sync authoritative data—not as the source of truth.
| Piece | Role |
|---|---|
| FCM | Delivery channel |
FirebaseMessagingService |
Receive, validate, route |
| Deep links | Navigation to target screen |
| Data vs notification messages | Background handling rules differ by OS version, priority, payload type, and OEM behavior |
Senior points: Notification channels, permission on Android 13+, dedupe, respect user opt-out, do not put secrets in payload.
A strong answer is:
FCM is a signal, not my source of truth. I handle notification permission and deep links, keep payloads non-sensitive, dedupe messages, and sync authoritative data when the app runs rather than assuming every background message gives me unrestricted execution.
Behavioral and leadership
Tell me about a production incident you resolved.
What interviewers are testing: Whether you narrate a production incident with measurable impact and systemic follow-up.
Use STAR with mobile-specific detail:
- Situation — spike in ANRs or crashes after release
- Task — restore stability without rolling back entire feature
- Action — Play Console stack traces, reproduce on low-RAM device, hotfix, staged rollout
- Result — crash-free rate recovered, postmortem, added test or monitor
Mention Firebase Crashlytics, Play vitals, or internal dashboards if you have them.
A strong answer is:
I describe a measurable incident—ANR or crash rate—with how I triaged stacks, shipped a fix, and added guardrails so the class of bug cannot silently return.
How do you balance feature delivery with tech debt?
What interviewers are testing: Whether you prioritize debt by user and team velocity risk—not architectural purity. Seniors are judged on trade-offs, not purity.
| Framework | Example |
|---|---|
| Risk-based | Pay debt touching crash or security first |
| Boy scout rule | Small cleanup in every feature PR |
| Dedicated capacity | Explicitly reserve capacity where recurring platform/debt work warrants it |
| Metrics | Build time, crash rate, lead time |
A strong answer is:
I prioritize debt that affects users or velocity—crashes, build times, flaky tests—and negotiate visible platform wins alongside features.
What is predictive back and why does it matter in modern Android navigation?
What interviewers are testing: Whether you integrate system Back, predictive gestures, and navigation state restoration consistently. Predictive back lets users preview where the system Back gesture will take them before completing the gesture.
| Topic | Senior talking point |
|---|---|
| System Back | Must integrate with app back stack correctly |
| Navigation Compose / Navigation 3 | Choose based on app architecture/migration state; integrate correctly with system Back and predictive-back APIs |
| Predictive back | Requires proper back handling and animations |
| State restoration | Preserve navigation state across recreation/process death where needed |
Android's current guidance points new Compose apps toward modern single-Activity navigation with correct back-stack behavior rather than ad hoc per-screen back handling.
A strong answer is:
I design navigation so system Back, predictive back gestures, and restored navigation state all behave consistently—especially in single-Activity Compose apps.
Final prep checklist
Technical drills:
- Explain lifecycle vs process death with Room recovery
- Whiteboard MVVM data flow from Compose to network
- Compare StateFlow vs SharedFlow and launch vs async
- Walk through Compose side effects and state hoisting
- Sketch offline-first sync with outbox + WorkManager
- Name Paging 3 + RemoteMediator responsibilities
- One performance story (jank, ANR, startup)
Cross-skill refresh:
- Java / JVM fundamentals for OOP and threading depth
- Design patterns vocabulary
- Git for CI and review flow
- Full stack guide if the role includes backend APIs
Behavioral:
- Three STAR stories—incident, conflict, technical decision
- Portfolio or Play Store links with 2-minute walkthrough each
Pattern cheat sheet (quick reference)
| Pattern | Tool / concept |
|---|---|
| Survive rotation | ViewModel, rememberSaveable |
| Survive process death | Room, DataStore, SavedStateHandle |
| UI state | StateFlow, sealed UiState |
| One-off events | Prefer UI state over ViewModel Channel buses |
| Offline-first | Room source of truth + sync worker |
| List at scale | Paging 3, keyset API |
| DI | Hilt constructor injection |
| Compose performance | Stable types, derivedStateOf, keys |
| Background work | WorkManager, not raw Service unless needed |
| Testing | runTest, fake repositories, Turbine |
References
- Android Developers — Guide
- Jetpack Compose
- Kotlin coroutines on Android
- Save UI state in Compose
- Side-effects in Compose
- WorkManager
- Android app architecture
- Android security best practices
- Navigation and predictive back
- Paging 3
Summary
Senior Android interviews connect Kotlin, Compose, coroutines, and data architecture to real device constraints—process death, offline sync, and main-thread discipline show up as scenario questions. Answer aloud and compare your structure to each section. Pair with JVM depth from our Java guides when interviewers cross into concurrency or OOP.

