AND

Master the job description

Study Guide

Every requirement a senior Android role commonly asks for — explained at senior depth, then tied to how you'd apply it on a real team. Read the concept, then the “in practice” line so you can speak from experience.

Depth over trivia Senior and staff Android interviews probe how you reason — why StateFlow over LiveData, when recomposition actually costs you, how you survive process death, and where coroutines leak. Each section below pairs the concept with an in practice note so you can answer from a real engineering decision, not a definition.

01 · Kotlin language essentials

What they want: idiomatic, production Kotlin — not Java written in Kotlin syntax. Fluency with val vs var, immutability by default, null-safety (?., ?:, !! as a code smell), data classes, sealed hierarchies for modelling state, and the scope functions (let, run, with, apply, also).

  • Prefer val and immutable collections; expose List, store MutableList privately.
  • Model exhaustive state with sealed interface + when (no else branch → compiler enforces completeness).
  • Delegation: by lazy, by viewModels(), property delegates, and class delegation via by.
  • inline + reified to keep generic type info at runtime; understand the cost (code-size) of inlining.
In practice A senior answer ties null-safety to crash rate: the type system pushes nullability to the boundary (parsing, platform types from Java) so the rest of the app is non-null by construction. !! in review is a red flag — it converts a compile-time guarantee back into a runtime crash.

02 · Coroutines & structured concurrency

What they want: deep coroutines — not just launch { }. Structured concurrency means every coroutine has a parent CoroutineScope; cancelling the scope cancels its children, and a child failure cancels siblings (unless a SupervisorJob isolates them).

  • suspend functions must be main-safe: they can be called on the main thread and internally switch with withContext(Dispatchers.IO).
  • Dispatchers: Main (UI), IO (blocking I/O, large pool), Default (CPU work), Unconfined (rarely).
  • Cancellation is cooperative: check isActive / call ensureActive(), and never swallow CancellationException.
  • coroutineScope vs supervisorScope; async/await for parallel decomposition.
Land this viewModelScope and lifecycleScope are structured scopes wired to a lifecycle — they cancel automatically, which is why you almost never need GlobalScope (which leaks because nothing cancels it).
In practice The senior framing: "I let the scope own cancellation." A network call started in viewModelScope is cancelled when the ViewModel clears, so a config change or back-press doesn't leak a request or update a dead UI.

03 · Flow, StateFlow & SharedFlow

What they want: reactive streams done right. Flow is a cold asynchronous stream; operators (map, filter, combine, flatMapLatest) run in the collector's context unless you flowOn.

StateFlowSharedFlow
Holds valueAlways (initial required)Optional replay
ConflationConflated, distinct-until-changedConfigurable buffer
Use forObservable UI stateOne-shot events (navigate, toast)
  • Collect with repeatOnLifecycle(STARTED) (or collectAsStateWithLifecycle() in Compose) so collection stops in the background — not launchWhenStarted (deprecated, keeps upstream hot).
  • stateIn(scope, SharingStarted.WhileSubscribed(5000), initial) shares a cold flow as state and stops upstream 5s after the last subscriber — surviving rotation without re-fetching.
In practice The classic bug: collecting a flow in onCreate with a plain launch keeps the collector alive in the background, wasting work and risking stale UI updates. repeatOnLifecycle is the fix interviewers want to hear.

04 · Jetpack Compose — composition & recomposition

What they want: the mental model. A @Composable is a function that describes UI from state; Compose builds a tree and recomposes (re-invokes) only the functions whose inputs changed. There is no view hierarchy you mutate — you change state and the framework reconciles.

  • Recomposition scope: the smallest enclosing composable that reads a given state. Reading state low in the tree keeps recomposition local.
  • Stability: Compose skips a composable if its params are @Stable/@Immutable and unchanged. Unstable types (e.g. List from a non-stable source) defeat skipping → use ImmutableList / kotlinx.collections.immutable.
  • Phases: Composition → Layout → Drawing. Defer reads to the latest phase (e.g. Modifier.offset { } lambda) to skip recomposition on scroll/animation.
New concept Donut-hole skipping: a parent can recompose without recomposing stable children. Internalize "what reads this state" and you can explain jank precisely instead of memoizing blindly.
In practice Turn on Compose compiler metrics / Layout Inspector recomposition counts. A senior debugs an over-recomposing list by finding the unstable parameter, not by sprinkling remember.

05 · Compose state & side-effects

What they want: state hoisting and the side-effect APIs. remember caches across recomposition; rememberSaveable survives config change/process death. Hoist state up (stateless composables take value + onValueChange) so UI is reusable and testable.

  • LaunchedEffect(key) — run a suspend block tied to composition; restarts when key changes, cancels on leave.
  • rememberCoroutineScope() — launch from callbacks (e.g. button click), scoped to composition.
  • DisposableEffect — register/unregister (listeners, observers) with an onDispose.
  • derivedStateOf — compute from other state and only recompose when the result changes (e.g. showButton = scrollOffset > 0).
  • produceState — bridge a non-Compose async source into State; snapshotFlow — turn Compose state into a Flow.
In practice The interview trap: launching a coroutine directly in composition body instead of LaunchedEffect — it fires on every recomposition. Saying "effects belong in effect handlers keyed correctly" signals real Compose maturity.

06 · Activity / Fragment lifecycle & configuration changes

What they want: the lifecycle cold. Activity: onCreate → onStart → onResume (foreground) → onPause → onStop → onDestroy. A configuration change (rotation, dark mode, locale, font scale) destroys and recreates the Activity by default.

  • Fragment view lifecycle is separate from the fragment: use viewLifecycleOwner for observers, or you leak when the view is destroyed but the fragment isn't.
  • Don't fight config changes with android:configChanges unless you truly own the redraw — prefer surviving state in a ViewModel.
  • onSaveInstanceState(Bundle) persists small UI state across recreation and process death (parcelable, < ~1MB).
In practice Senior answer: "Rotation isn't special — it's the same recreation path as process death with more state retained. If my state survives process death via SavedStateHandle, rotation is free."

07 · ViewModel, SavedStateHandle & process death

What they want: where state lives and how it survives. A ViewModel outlives configuration changes (scoped to the ViewModelStoreOwner) but is cleared when the owner is truly finished. It does not survive process death on its own — the OS can kill a backgrounded app to reclaim memory.

  • SavedStateHandle is the bridge: a key-value map backed by saved instance state, so values survive process death. Inject it into the ViewModel and expose state via getStateFlow(key, default).
  • Never hold a Context/View/Activity reference in a ViewModel (leak). Use AndroidViewModel's application context only if unavoidable.
  • Test the restore path: kill the process from dev tools ("Don't keep activities") and confirm the screen rebuilds correctly.
Land this ViewModel survives rotation; SavedStateHandle survives process death; a database/DataStore survives everything. Naming the three tiers is the senior tell.
In practice The realistic failure: a search screen loses the query after the OS kills the app in the background. Put the query in SavedStateHandle and it restores — the fix interviewers are listening for.

08 · Architecture — MVVM, MVI & Clean Architecture

What they want: a defensible architecture with unidirectional data flow. MVVM: the ViewModel exposes observable state; the View renders it and sends events up. MVI tightens this into a single immutable UiState + an Intent/Action stream reduced into new state — great for complex, experiment-heavy screens.

  • Clean Architecture layers: UIDomain (use-cases, pure Kotlin, no Android) → Data (repositories, sources). Dependencies point inward; the domain knows nothing about Retrofit or Room.
  • Single source of truth: the repository decides cache vs network; the UI never calls the API directly.
  • Model state as one object: data class UiState(val items: List, val loading: Boolean, val error: String?) — render the whole thing, avoid scattered booleans.
In practice Pragmatism wins: "I use MVVM with a single immutable state per screen and use-cases only where domain logic is non-trivial — I don't add a use-case that just forwards a repo call." That nuance reads as staff-level judgment, not dogma.

09 · Dependency injection — Hilt & Dagger

What they want: DI to keep code testable and decoupled. Hilt sits on Dagger and generates components tied to Android lifecycles: @HiltAndroidApp, @AndroidEntryPoint, @HiltViewModel, and scopes (@Singleton, @ActivityRetainedScoped, @ViewModelScoped).

  • @Module + @Provides for things you don't own (Retrofit, OkHttp); @Binds for interface→impl.
  • Constructor injection (@Inject constructor) everywhere you can — it's the most testable.
  • Qualifiers (@Named / custom) disambiguate multiple bindings of the same type (e.g. two OkHttp clients).
Land this The point of DI isn't "objects get created for me" — it's dependency inversion: high-level code depends on interfaces, so tests swap a fake repository in one line. Frame DI as a testability and modularity tool.
In practice Hilt's compile-time graph means a missing binding is a build error, not a runtime crash — a real advantage over service-locator patterns you should name.

10 · Persistence — Room & DataStore

What they want: local storage chosen correctly. Room is the SQLite ORM: @Entity, @Dao, @Query, with compile-time SQL verification and Flow return types for reactive reads. DataStore replaces SharedPreferences: Preferences DataStore (key-value) or Proto DataStore (typed), both async and transactional.

  • Room DAOs returning Flow<List<T>> emit on every change — the backbone of offline-first.
  • Migrations: provide a Migration or risk a crash on schema change; export the schema for tests.
  • Never do disk I/O on the main thread — Room suspend DAOs and DataStore are main-safe by design.
In practice "SharedPreferences is synchronous and silently does I/O on the main thread (apply() defers the disk write but the in-memory commit still blocks). DataStore fixes that with a Flow + coroutines." That contrast is a frequent senior question.

11 · Networking — Retrofit, OkHttp & serialization

What they want: a robust network layer. Retrofit defines a typed API interface; OkHttp is the engine with the connection pool, cache, and interceptors. Serialize with Moshi or kotlinx.serialization.

  • Interceptors: an application interceptor for auth headers/logging; a network interceptor for cache control. Add a token-refresh Authenticator for 401s.
  • Model results with a sealed Result/Either type — never let raw exceptions reach the UI.
  • Map DTOs → domain models in the repository; DTOs never leak into Compose.
  • OkHttp HTTP cache + ETags for conditional GETs; retry only idempotent calls with backoff.
In practice Offline-first: the repository reads from Room (source of truth) and refreshes from the network, writing back to the DB so the UI updates via its Flow — the user never stares at a spinner, with stale-while-revalidate semantics.

12 · Navigation & deep linking

Core: Navigation-Compose models the app as a graph of destinations with typed routes (type-safe routes via @Serializable route objects in Navigation 2.8+). The NavController owns the back stack; you pass arguments and pop with results.

  • Hoist the NavController at the graph root; pass lambdas (onNavigateToDetail) into screens so composables stay navigation-agnostic and testable.
  • Deep links: declare <intent-filter> + android:autoVerify for App Links (verified https) and a navDeepLink mapping the URI to a destination.
  • Nested graphs for feature modules; SavedStateHandle receives nav args inside the ViewModel.
In practice Auth-gating is a graph decision, not a hidden screen: a logged-out user gets the auth graph as the start destination. App Links need the assetlinks.json on your domain — a step people forget.

13 · Background work — WorkManager, Services & broadcasts

What they want: the right tool for off-screen work. WorkManager is the default for deferrable, guaranteed work (sync, upload) — it survives process death and reboot, respects constraints (network, charging), and backs off on failure.

  • OneTimeWorkRequest vs PeriodicWorkRequest (min 15-min interval); chain with beginWith().then(); unique work to dedupe.
  • Foreground services for user-visible ongoing work (playback, navigation) — require a notification and, on Android 14+, a declared foregroundServiceType.
  • Modern background limits: JobScheduler/WorkManager over implicit wake-locks; BroadcastReceiver for system events, but most implicit broadcasts are restricted.
In practice "Don't use a Service for a one-off network sync — that's WorkManager. Use a foreground service only when the user can see it." Knowing the boundary (and Doze/App Standby) is the senior signal.

14 · Performance — jank, frame timing & baseline profiles

Core: the metric is frames rendered within the 16.6ms budget (60fps; 8.3ms at 120Hz). Jank = a dropped frame, surfaced by JankStats, the Macrobenchmark library, and Perfetto traces.

  • Baseline Profiles: ship AOT-compilation hints for hot paths so the first runs aren't interpreted — major cold-start and scroll wins, generated via Macrobenchmark.
  • Compose: avoid unstable params, defer state reads to layout/draw, use LazyColumn keys, and remember expensive computations.
  • Main-thread discipline: no I/O, no large JSON parse, no bitmap decode on the UI thread.
New concept Measure on a release build with R8 on — debug builds are misleadingly slow and Compose isn't fully optimized. Macrobenchmark drives a real install.
In practice "I profile with Perfetto/Macrobenchmark, fix the proven hot frame, then add a baseline profile and a benchmark to guard it." Profiling-first beats guessing — the line interviewers want.

15 · Memory leaks, ANRs & profiling

Core: two failure modes. Leaks retain objects the GC should free — the classic Android leak is a Context/View held past its lifecycle (static refs, inner classes, callbacks, a Handler posting delayed runnables). ANRs fire when the main thread is blocked > 5s (input dispatch) — almost always main-thread I/O or lock contention.

  • LeakCanary in debug auto-detects retained instances and dumps the reference chain — name it as your first tool.
  • Android Studio Memory Profiler + heap dumps for cumulative growth; ANR traces in Play Console / /data/anr/traces.txt.
  • Common coroutine leak: launching in GlobalScope or collecting a flow without lifecycle awareness.
In practice Leaks present as slow, cumulative jank and OOM crashes after long sessions — you hunt them over time with LeakCanary, not from a single stack trace. ANRs you fix by getting work off the main thread.

16 · App startup & R8 / ProGuard

Core: cold start = process fork → Application.onCreate → first frame. The budget is spent on eager initialization, large dependency graphs, and main-thread work.

  • Use the App Startup library to consolidate and order initializers (and lazy-init the ones not needed at launch).
  • R8 shrinks, optimizes, and obfuscates: dead-code/resource removal cuts size and improves load; keep rules (-keep) protect reflection (Gson, serialization).
  • Defer non-critical work off the launch path; show content fast and hydrate the rest.
In practice "I measure cold start on a release build with Macrobenchmark, trim Application.onCreate, add a baseline profile, and keep R8 on." Treating startup as a defended budget is a senior tell.

17 · Testing — JUnit, MockK, Turbine & Compose UI tests

What they want: a real test pyramid. Many fast unit tests (JUnit + MockK) on ViewModels, use-cases, and mappers; fewer integration tests; a few UI/E2E (Compose test or Espresso) on critical flows.

  • Coroutines/Flow: runTest + a TestDispatcher (inject the dispatcher, never hardcode Dispatchers.IO); assert emissions with Turbine.
  • Fakes vs mocks: prefer a hand-written fake repository for state-based tests; use mocks for interaction verification. Over-mocking couples tests to implementation.
  • Robolectric runs the Android framework on the JVM (fast, no device); Compose UI test uses semantics (onNodeWithText, assertIsDisplayed).
In practice The injectable-dispatcher pattern is the senior tell: class Repo(private val io: CoroutineDispatcher) so tests pass StandardTestDispatcher() and control virtual time. Hardcoded dispatchers make tests flaky.

18 · Gradle — variants, version catalogs & modularization

What they want: you can reason about the build. Build variants = build types (debug/release) × product flavors (free/paid, staging/prod). Version catalogs (libs.versions.toml) centralize dependency versions.

  • KSP vs kapt: KSP is the Kotlin-native annotation processor — much faster than kapt (which generates Java stubs). Room/Hilt/Moshi support KSP; migrate off kapt for build speed.
  • Modularization: split by feature (:feature:home) and layer (:core:data) for parallel builds, faster incremental compilation, and enforced boundaries.
  • Convention plugins (build-logic) DRY up module config; api vs implementation controls what leaks transitively.
In practice "Modularization buys parallel + incremental builds and architectural enforcement — a :feature module physically can't import another feature's internals." Naming build-speed and boundaries is the staff answer.

19 · Security — Keystore, encryption & integrity

Core: the APK ships to the device and can be decompiled — never hardcode secrets. Store keys in the Android Keystore (hardware-backed where available); encrypt local data with Jetpack Security (EncryptedSharedPreferences / EncryptedFile) or SQLCipher for Room.

  • Tokens: short-lived access + refresh; gate sensitive actions behind BiometricPrompt.
  • Network: Network Security Config + certificate pinning (mind rotation), HTTPS only, no cleartext.
  • Play Integrity API attests a genuine app/device; obfuscation (R8) and root detection are defense-in-depth, not silver bullets.
  • Know the OWASP MASVS / Mobile Top 10 vocabulary: insecure storage, weak crypto, etc.
In practice "Parse the threat model out loud": secrets-in-bundle, data at rest, data in transit, and device integrity are four distinct buckets. Naming them shows security maturity.

20 · Accessibility & the on-device AI frontier

Core: ship for TalkBack. In Compose, set Modifier.semantics { contentDescription = ... }, mark headings, group related nodes, honor touch-target size (48dp), Dynamic Type (sp units), and contrast (WCAG AA).

  • Modifier.clickable(onClickLabel=...), stateDescription, and merging semantics for composite controls.
  • Test with the Accessibility Scanner and Compose semantics assertions.

Frontier: on-device generative AI via Gemini Nano (AICore / ML Kit GenAI APIs) and the MediaPipe LLM Inference API, plus classic TensorFlow Lite (LiteRT) for vision/audio. The trade-off: privacy (data stays local), offline, no server cost — against model size, RAM, and device fragmentation.

In practice "Gemini Nano is gated to capable devices (Pixel 8+/flagships via AICore); I feature-detect and fall back to a server model otherwise." Reasoning about the privacy/latency/cost trade-off and device support is the staff-level answer.