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 45 questions with sample answers aligned with what candidates report in 2026 loops: Kotlin depth, Jetpack Compose, coroutines and Flow, architecture, DI, offline-first design, performance, testing, and mobile 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?
Loops vary by company size, but a common pattern looks like this:
| 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?
A senior prep plan should produce spoken narratives and one reference architecture, not only flashcards.
| 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 |
| 6 | Mocks + STAR stories + Git CI basics | Two timed mocks with verbal walkthrough |
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 |
|---|---|---|
| OOP, interfaces | data class, sealed hierarchies |
Java OOP interviews |
| Concurrency | Coroutines, Flow | Threads, executors in Java part 2 |
| Patterns | Delegation, extension functions | Design patterns in Java |
Interview tip: When asked about threading, connect coroutines as compiler-transformed state machines to why they scale better than raw threads for I/O.
Kotlin language depth
Explain Kotlin null safety—how is it different from Java?
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?
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?
inline copies bytecode at the call site—removing lambda allocation overhead and enabling reified generics at runtime.
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 everything should be inline.
A strong answer is:
inline plus reified lets me access T::class at runtime without passing Class objects—common for type-safe parsing and Android parcelable helpers.
Extension functions vs utility classes—when do you use each?
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?
Interviewers care less about reciting every callback and more about state survival and resource cleanup.
| Callback | Senior talking point |
|---|---|
onCreate |
One-time setup; restore from savedInstanceState |
onStart / onStop |
Visible but may not be interactive |
onResume / onPause |
Foreground interaction; pause heavy work |
onDestroy |
Final cleanup; may be skipped on configuration change |
Configuration change: Without ViewModel or rememberSaveable, rotation recreates the Activity—dropping in-memory state.
Process death: OS may kill your process under memory pressure. Disk (Room, DataStore, SavedStateHandle) survives; plain Activity fields do not.
A strong answer is:
I focus on what survives rotation vs process death. ViewModel and persisted storage survive configuration changes; only persisted state survives process death.
What is the role of ViewModel—and what should NOT go in it?
ViewModel survives configuration changes and exposes UI state to the screen.
Belongs in ViewModel:
- UI state (
StateFlow,UiState) - Calls to repositories / use cases
- Coroutine work in
viewModelScope
Does not belong:
Context,View, orActivityreferences (leaks)- Navigation side effects without a clear pattern (prefer one-off events channel)
- 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?
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, SavedStateHandle (small UI state) |
| No | In-memory singletons, static caches, uncleared ViewModel if not restored |
Senior pattern: Single source of truth in Room; UI observes Flow from DB. Network refresh updates DB—not a fragile in-memory list.
Test: Enable "Don't keep activities" and "Background process limit" in developer options; walk through your critical flows.
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?
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?
Compose runs in three phases for each frame:
| Phase | What happens |
|---|---|
| Composition | Build or update the UI tree (run @Composable functions) |
| Layout | Measure and place nodes |
| Draw | Paint pixels |
Performance angle: Skipping recomposition is cheaper than laying out or drawing again. Stability and smart state reads reduce composition work.
A strong answer is:
Composition decides what UI should exist; layout positions it; draw renders it. Senior performance work targets skipping unnecessary composition first.
What triggers recomposition—and how do you reduce it?
Recomposition runs when state read during composition changes.
Reduction tactics:
| Technique | Purpose |
|---|---|
| Stable / immutable models | Compiler can skip unchanged composables |
remember |
Cache expensive objects across recompositions |
derivedStateOf |
Recompute only when dependencies change |
| 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 derivedStateOf for derived UI flags, and profile with Layout Inspector when lists jank.
Explain state hoisting with a concrete example.
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?
| API | Survives |
|---|---|
remember |
Recomposition and configuration change (same process) |
rememberSaveable |
Also process death via SavedStateRegistry (Bundle-sized data) |
Use rememberSaveable for small UI state—scroll position, expanded flags, draft text—not large lists (use Room).
Parcelable / list savers exist for custom types—mind Bundle size limits.
A strong answer is:
remember is in-process only. rememberSaveable survives process death for small UI state; large data belongs in Room or DataStore.
LaunchedEffect vs DisposableEffect vs SideEffect?
Compose side-effect APIs tie work to the composition lifecycle:
| API | Use when |
|---|---|
LaunchedEffect(key) |
Suspend work tied to keys—load data, collect Flow |
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(userId) {
viewModel.loadUser(userId)
}
DisposableEffect(lifecycleOwner) {
val observer = LifecycleEventObserver { _, event -> /* ... */ }
lifecycleOwner.lifecycle.addObserver(observer)
onDispose { lifecycleOwner.lifecycle.removeObserver(observer) }
}A strong answer is:
LaunchedEffect for coroutine work keyed to composition; DisposableEffect when I must unregister on dispose; rememberUpdatedState to avoid stale lambdas in effects.
How do you migrate from Views to Compose incrementally?
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?
Structured concurrency means every coroutine has a parent scope—when the scope cancels, children cancel too. No 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.
A strong answer is:
Structured concurrency ties async work to a lifecycle scope. I use viewModelScope or lifecycleScope—not GlobalScope—for UI-driven coroutines.
Explain coroutine Dispatchers—Main, IO, Default.
| Dispatcher | Use for |
|---|---|
Dispatchers.Main |
UI updates, fast work on main thread |
Dispatchers.IO |
Blocking I/O—network, disk, database |
Dispatchers.Default |
CPU-heavy work—parsing, sorting, crypto |
suspend fun loadFeed(): List<Post> = withContext(Dispatchers.IO) {
api.fetchPosts()
}Senior nuance: Suspend does not mean background—only withContext or dispatcher choice moves work off Main. Room and Retrofit provide main-safe suspend APIs when used correctly.
A strong answer is:
Main for UI, IO for blocking I/O, Default for CPU. I use withContext explicitly when library code might block.
launch vs async—when do you use each?
| 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 exceptions surface at await()—use supervisorScope when one failure should not cancel siblings.
A strong answer is:
launch for side effects; async when I need parallel results and will await Deferred. I handle failures at await and consider supervisorScope for partial success.
Which Flow operators do you use most in production?
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 { q -> repo.search(q) }
.catch { emit(emptyList()) }A strong answer is:
debounce plus flatMapLatest for search, distinctUntilChanged for UI, catch for graceful degradation—I explain backpressure and cancellation, not just operator names.
How do you test coroutines and Flow?
| 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: Inject dispatchers—do not hardcode Dispatchers.IO inside ViewModels if you want fast unit tests.
A strong answer is:
I use runTest with 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?
| 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?
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: Inner layers never depend on outer layers. Domain defines repository interfaces; data module implements them.
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?
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 { it ?: User.empty(id) }
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.
How do you modularize a large Android app?
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: Feature modules do not depend on each other directly—navigate via interfaces or deep links. Share only through core APIs.
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?
Dependency injection supplies dependencies from the outside—testable, swappable, lifecycle-aware.
| Hilt component | Scope |
|---|---|
SingletonComponent |
App-wide (OkHttp, Room) |
ActivityRetainedComponent |
ViewModel |
ViewModelComponent |
ViewModel-specific deps |
@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?
Offline-first apps read from disk first and refresh in the background.
| 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?
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 | Guaranteed retry, battery-aware |
| API | Version vectors or ETags |
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?
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?
| Approach | When |
|---|---|
| Paging 3 library | Standard RecyclerView/Compose lists—keys, placeholders, retry |
| Cursor / keyset API | Large feeds, stable ordering |
| Offset | Simple admin lists only—poor at scale |
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?
| Symptom | Tool / action |
|---|---|
| ANR | Main thread blocked—check Play Console traces, StrictMode |
| Jank | Systrace, Perfetto, Android Studio Profiler |
| Compose | Layout Inspector recomposition counts |
Common causes: Disk/network on Main, over-recomposition, bitmap work on UI thread, lock contention.
Fix pattern: Move work to Dispatchers.Default/IO, use Baseline Profiles for startup, lazy list keys, image loading library with sizing.
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?
| Cause | Prevention |
|---|---|
| Static reference to Activity | Avoid; use Application context carefully |
| Listener not removed | DisposableEffect, onDestroy cleanup |
| 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?
| Topic | Practice |
|---|---|
| Network | TLS, certificate pinning (when justified) |
| Storage | EncryptedSharedPreferences / EncryptedFile for secrets |
| WebView | Disable risky JS bridges; validate URLs |
| Exported components | Minimize exported Activities/Services |
| Secrets | No API keys in APK—remote config or attestation patterns |
Proguard/R8: Obfuscation is not encryption—assume reverse engineering.
A strong answer is:
I encrypt sensitive local data, avoid hard-coded secrets, minimize exported surfaces, and treat R8 as obfuscation—not a vault.
Testing and delivery
What is your Android testing strategy?
| 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 unit tests on every PR; shard instrumentation tests nightly.
A strong answer is:
Heavy unit coverage on ViewModels and repositories; fewer UI tests on money paths. I inject fakes and use runTest for coroutines.
What CI/CD steps matter for Android teams?
Typical pipeline:
- Lint (Android Lint, detekt, ktlint)
- Unit tests
- Assemble debug/release
- Instrumentation (optional per PR)
- Sign & distribute (Firebase App Distribution, Play Internal)
Versioning: semantic versionCode monotonic for Play Store.
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 versionCode and go through staged rollout. I tie Git flow to what CI actually gates.
Mobile system design scenarios
Design an offline-first news feed.
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 API, ETag per page |
| 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.
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.
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?
| 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 |
Senior points: Notification channels, permission on Android 13+, dedupe, respect user opt-out, do not put secrets in payload.
A strong answer is:
FCM delivers; the app routes through a single handler to navigation and analytics. I design for permission denial and data-only messages in background.
Behavioral and leadership
Tell me about a production incident you resolved.
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?
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 | 10–20% sprint for platform work |
| 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.
Final-week checklist for senior Android interviews?
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
A strong answer is:
In the final week I rehearse offline-first design aloud, drill Compose and coroutines trade-offs, and polish three STAR stories tied to shipped apps—not slides of API trivia.
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 | Channel, SharedFlow(replay=0) |
| 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
Official Android documentation
- Android Developers — Guide
- Jetpack Compose
- Kotlin coroutines on Android
- Guide to app architecture
- Paging 3
On-site prep
- Java interview questions — part 1
- Java interview questions — part 2
- Design patterns in Java
- Full stack developer interviews
- Git interview questions
- Technical specialist interviews
- Interview Questions category
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.

