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.
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
valand immutable collections; exposeList, storeMutableListprivately. - Model exhaustive state with
sealed interface+when(noelsebranch → compiler enforces completeness). - Delegation:
by lazy,by viewModels(), property delegates, and class delegation viaby. inline+reifiedto keep generic type info at runtime; understand the cost (code-size) of inlining.
!! 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/ callensureActive(), and never swallowCancellationException. coroutineScopevssupervisorScope;async/awaitfor parallel decomposition.
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).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.
| StateFlow | SharedFlow | |
|---|---|---|
| Holds value | Always (initial required) | Optional replay |
| Conflation | Conflated, distinct-until-changed | Configurable buffer |
| Use for | Observable UI state | One-shot events (navigate, toast) |
- Collect with
repeatOnLifecycle(STARTED)(orcollectAsStateWithLifecycle()in Compose) so collection stops in the background — notlaunchWhenStarted(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.
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/@Immutableand unchanged. Unstable types (e.g.Listfrom a non-stable source) defeat skipping → useImmutableList/kotlinx.collections.immutable. - Phases: Composition → Layout → Drawing. Defer reads to the latest phase (e.g.
Modifier.offset { }lambda) to skip recomposition on scroll/animation.
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 whenkeychanges, cancels on leave.rememberCoroutineScope()— launch from callbacks (e.g. button click), scoped to composition.DisposableEffect— register/unregister (listeners, observers) with anonDispose.derivedStateOf— compute from other state and only recompose when the result changes (e.g.showButton = scrollOffset > 0).produceState— bridge a non-Compose async source intoState;snapshotFlow— turn Compose state into a Flow.
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
viewLifecycleOwnerfor observers, or you leak when the view is destroyed but the fragment isn't. - Don't fight config changes with
android:configChangesunless you truly own the redraw — prefer surviving state in a ViewModel. onSaveInstanceState(Bundle)persists small UI state across recreation and process death (parcelable, < ~1MB).
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.
SavedStateHandleis the bridge: a key-value map backed by saved instance state, so values survive process death. Inject it into the ViewModel and expose state viagetStateFlow(key, default).- Never hold a
Context/View/Activity reference in a ViewModel (leak). UseAndroidViewModel'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.
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: UI → Domain (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.
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+@Providesfor things you don't own (Retrofit, OkHttp);@Bindsfor 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).
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
Migrationor risk a crash on schema change; export the schema for tests. - Never do disk I/O on the main thread — Room
suspendDAOs and DataStore are main-safe by design.
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
Authenticatorfor 401s. - Model results with a
sealedResult/Eithertype — 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.
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
NavControllerat the graph root; pass lambdas (onNavigateToDetail) into screens so composables stay navigation-agnostic and testable. - Deep links: declare
<intent-filter>+android:autoVerifyfor App Links (verified https) and anavDeepLinkmapping the URI to a destination. - Nested graphs for feature modules;
SavedStateHandlereceives nav args inside the ViewModel.
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.
OneTimeWorkRequestvsPeriodicWorkRequest(min 15-min interval); chain withbeginWith().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;BroadcastReceiverfor system events, but most implicit broadcasts are restricted.
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
LazyColumnkeys, andrememberexpensive computations. - Main-thread discipline: no I/O, no large JSON parse, no bitmap decode on the UI thread.
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
GlobalScopeor collecting a flow without lifecycle awareness.
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.
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+ aTestDispatcher(inject the dispatcher, never hardcodeDispatchers.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).
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;apivsimplementationcontrols what leaks transitively.
: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.
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.