20 frequently asked Mobile Developer interview questions. Mobile development is an in-demand field focused on building reliable iOS, Android, and cross-platform apps, solving product, performance, release, offline, and device-integration tasks. The questions cover different levels, and you can practice answering them aloud in our interview trainer.
1What is the difference between storing an authentication token in standard key-value storage like SharedPreferences or UserDefaults versus platform secure storage like Android Keystore or iOS Keychain?
Standard storage mechanisms like Android's SharedPreferences and iOS's UserDefaults are designed for lightweight, non-sensitive preferences. They store data in unencrypted plaintext files (XML or plist) within the application's sandbox directory. Anyone with physical access to a rooted or jailbroken device, an unencrypted device backup, or file-system access can read these tokens directly. In contrast, platform secure storage—specifically iOS Keychain and Android Keystore (frequently utilized via EncryptedSharedPreferences)—provides data-at-rest encryption. iOS Keychain encrypts stored items using keys tied to the device hardware (such as the Secure Enclave) and allows fine-grained access policies. Android Keystore generates and stores cryptographic keys inside hardware-isolated security modules (Trusted Execution Environment or StrongBox), ensuring cryptographic keys are never exposed in application memory and cannot be extracted from the file system.
// --- ANDROID ---
// INSECURE (Plain SharedPreferences):
val prefs = context.getSharedPreferences("app_prefs", Context.MODE_PRIVATE)
prefs.edit().putString("auth_token", token).apply() // Plaintext XML on disk
// SECURE (EncryptedSharedPreferences backed by Android Keystore):
val masterKey = MasterKey.Builder(context)
.setKeyScheme(MasterKey.KeyScheme.AES256_GCM)
.build()
val securePrefs = EncryptedSharedPreferences.create(
context,
"secure_prefs",
masterKey,
EncryptedSharedPreferences.PrefKeyEncryptionScheme.AES256_SIV,
EncryptedSharedPreferences.PrefValueEncryptionScheme.AES256_GCM
)
securePrefs.edit().putString("auth_token", token).apply()
// --- IOS (Swift) ---
// INSECURE:
UserDefaults.standard.set(token, forKey: "auth_token") // Plain plist on disk
// SECURE (Keychain Services API):
let query: [String: Any] = [
kSecClass as String: kSecClassGenericPassword,
kSecAttrAccount as String: "user_auth_token",
kSecValueData as String: token.data(using: .utf8)!,
kSecAttrAccessible as String: kSecAttrAccessibleWhenUnlocked
]
SecItemAdd(query as CFDictionary, nil)
2What is the primary purpose of a mobile CI/CD pipeline, and what unique stages and constraints distinguish it from web or backend deployment pipelines?
The primary purpose of a mobile CI/CD pipeline is to automate the building, testing, signing, and distribution of mobile client applications to maintain consistent quality and repeatable releases. Continuous Integration (CI) validates code changes through automated builds, static analysis, and unit testing. Continuous Delivery/Deployment (CD) packages, signs, and distributes binary artifacts to testing tracks (like TestFlight or Firebase App Distribution) or app stores. Mobile CI/CD differs from web and backend pipelines in several key ways: 1. Artifact Type: Builds produce compiled client binaries (.ipa, .apk, .aab) rather than running directly on servers or inside container images. 2. Runner Hardware Constraints: iOS compilation requires macOS hardware and Xcode command-line tools, whereas backend pipelines typically run on lightweight Linux containers. 3. Release Latency and Approvals: Publishing to end-users involves third-party app store review cycles (Apple App Store / Google Play Store), meaning deployments cannot be rolled back instantly via a server redeploy. Fixes require a new signed binary submission or runtime feature flags.
name: Mobile CI/CD Workflow
on:
pull_request:
branches: [main]
push:
tags:
- 'v*.*.*'
jobs:
ci_validation:
name: Lint & Unit Tests
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Run Linter
run: ./gradlew lint
- name: Run Unit Tests
run: ./gradlew testDebugUnitTest
cd_release_ios:
name: Build & Distribute iOS
needs: ci_validation
if: startsWith(github.ref, 'refs/tags/v')
runs-on: macos-14
steps:
- uses: actions/checkout@v4
- name: Install Signing Certificates
run: fastlane match appstore --readonly
- name: Build & Upload to TestFlight
run: fastlane ios beta
3In a cross-platform mobile application, what is the purpose of separating presentation, domain, and data layers, and how does this structure facilitate code sharing across iOS and Android?
Separating an application into presentation, domain, and data layers establishes clear boundaries of responsibility: 1. **Presentation Layer:** Handles UI rendering, user input, and screen-level state (e.g., Views, Widgets, ViewModels, or Presenters). 2. **Domain Layer:** Encapsulates pure business logic, domain models/entities, and use cases. It remains independent of UI and platform frameworks. 3. **Data Layer:** Manages data retrieval and persistence from remote APIs, local databases, or device cache via repositories and data sources. In cross-platform mobile development, this structure facilitates code sharing because the domain and data layers are platform-agnostic. Core business rules, data transformations, and network handling can be shared 100% across iOS and Android, allowing teams to either share the entire stack (as in Flutter or React Native) or share business/data logic while keeping platform-specific native presentation layers (as in Kotlin Multiplatform).
[Presentation Layer] (UI, Screens, ViewModels)
│
▼ calls
[Domain Layer] (Use Cases, Business Rules, Entities)
▲
│ implements interfaces
[Data Layer] (Repositories, API Clients, Local Storage)
4What are the core differences between imperative and declarative UI development paradigms in mobile engineering, and how do Flutter widgets and React Native components embody the declarative model?
In imperative UI development, developers write explicit, step-by-step instructions to create, mutate, and destroy UI elements (such as finding a view by ID and directly calling methods like setText or setVisibility). In contrast, declarative UI models describe what the UI should look like for a given state (often expressed as UI = f(state)). When state changes, the framework determines how to update the visual representation efficiently. Flutter and React Native both embody this declarative paradigm. In Flutter, widgets are immutable configuration descriptions; when dynamic data changes, calling setState() in a StatefulWidget schedules a rebuild where the build() method returns a new widget tree description that Flutter reconciles against its Element and RenderObject trees. In React Native, components are functions or classes returning JSX; updating state or props triggers a re-render where React reconciles the virtual element tree and applies the minimal necessary native updates across the bridge or native runtime.
5What is the operational difference between a cold start, a warm start, and a hot start in mobile applications, and why is cold start the most critical metric for performance budgets?
In mobile app lifecycles, startup states differ based on whether the app's process and memory state already exist in the OS. - Cold Start: The app starts from scratch because the OS has not created its process, or the process was previously killed. The OS must allocate a new process, load binaries/runtimes, initialize the Application/runtime context, and inflate the first view. It has the highest latency. - Warm Start: The app's process is usually already in memory, but the activity/view hierarchy was destroyed or evicted (e.g., due to background recreation or back navigation). The OS re-creates the UI/activity without needing to fork/spawn a new process from scratch. - Hot Start: The app and its UI state are still fully resident in memory (e.g., the user pressed Home and immediately returned). The OS merely brings the existing view hierarchy to the foreground with near-zero initialization overhead. Cold start is the most critical metric for performance budgets because it represents the slowest first-impression experience for users. Long cold starts directly correlate with immediate user drop-off, lower retention, and poor app store rankings (such as Android Vitals thresholds).
6What is a native bridge in a cross-platform mobile app, and when would you use one instead of writing the feature entirely in Flutter or React Native code?
A native bridge is the communication layer that lets cross-platform code call platform-specific iOS or Android code, and lets native code return results or send events back. In Flutter this is commonly done with platform channels or plugins. In React Native it is commonly done with native modules, TurboModules, or native UI components. You use a bridge when a feature needs something that pure Flutter or React Native code cannot access well enough, such as a platform API, device capability, native SDK, performance-sensitive native implementation, or native UI component. Common examples include camera features, Bluetooth, push notifications, payments, secure storage, background services, health APIs, or SDKs that only provide Swift/Objective-C or Kotlin/Java integrations. A good bridge usually exposes a small, clear API to the cross-platform layer, such as getBatteryLevel, startBluetoothScan, or openNativePaymentSheet, while the platform-specific implementation handles the real iOS and Android details.
7What does an offline-first architecture mean in mobile development, and how does it fundamentally differ from standard HTTP response caching?
In mobile development, an offline-first architecture treats local storage as the primary source of truth for both read and write operations. Instead of waiting for network requests to complete before rendering or allowing user interactions, the app interacts directly with the local database or storage layer, while background synchronization processes handle reconciling local changes with the remote server when connectivity is available. This fundamentally differs from standard HTTP response caching in several ways: 1. **Data Model**: HTTP caching stores raw network responses (e.g., JSON payloads) keyed by request URLs and headers. Offline-first architecture stores structured domain entities in a local database (like Room, SQLite, or SwiftData/Core Data). 2. **Writes vs. Reads**: HTTP caching is primarily a read-optimization mechanism and does not natively support offline write transactions or mutations. Offline-first allows full local writes, queuing mutations for later server synchronization. 3. **Queryability and Lifecycle**: Cached HTTP responses are subject to automatic cache eviction policies and cannot be arbitrarily queried, filtered, or joined. Offline-first persistence provides full querying, indexing, and deterministic lifecycle management independent of network status.
/* Network-First with HTTP Caching */
UI -> Network Request -> [Cache Hit ? Return Cached JSON : Fetch from API -> Save to HTTP Cache] -> UI
// Limitation: Read-only optimization; offline writes fail immediately.
/* Offline-First with Local Source of Truth */
UI <-> Observes Local DB (e.g., Room / Core Data / SQLite)
User Write -> Write to Local DB -> Queue Sync Task
Sync Worker (Background) -> Send queued mutations to Server -> Update Local DB with Server Response
8What is the difference between a remote push notification and a local notification in a mobile app?
The core difference between local and remote push notifications lies in where they originate and how they are triggered: 1. Local Notifications are created, scheduled, and triggered entirely on the device by the application using OS APIs (such as UserNotifications on iOS or AlarmManager/WorkManager/NotificationManager on Android). They trigger based on device-side conditions such as a specific date/time, a countdown timer, or a geographic boundary (geofencing). Because they run locally on the operating system daemon, they do not require a backend server or an active internet connection at delivery time. 2. Remote Push Notifications originate from an external application server and are transmitted over the internet via platform push gateways (APNs for Apple devices, FCM for Android). The gateway delivers the payload to the device's OS-level daemon, waking the app or posting a banner. Remote notifications are required for real-time external events such as incoming chat messages, friend requests, or breaking news.
let content = UNMutableNotificationContent()
content.title = "Workout Reminder"
content.body = "Time for your daily exercise routine."
content.sound = .default
// Triggers locally after 1 hour (3600 seconds) without any server interaction
let trigger = UNTimeIntervalNotificationTrigger(timeInterval: 3600, repeats: false)
let request = UNNotificationRequest(identifier: "dailyWorkout", content: content, trigger: trigger)
UNUserNotificationCenter.current().add(request) { error in
if let error = error {
print("Failed to schedule notification: \(error)")
}
}
9What is the structural and operational difference between an Android App Bundle (.aab), an APK, and an iOS IPA archive when distributing apps through app stores?
An APK (Android Package) is an executable package format containing compiled DEX bytecode, resources, assets, and native libraries that can be directly installed and run on an Android device. A universal APK bundles resources for all screen densities, CPU architectures, and languages, leading to larger download sizes. An Android App Bundle (.aab) is an upload/publishing format required by Google Play for store distribution. An AAB cannot be installed directly on a device; instead, Google Play uses the bundle and Play App Signing to generate optimized split APKs tailored dynamically to a specific device's architecture, screen density, and language (Dynamic Delivery). An iOS IPA (.ipa) is an application archive file (a zip container enclosing the Payload directory, `.app` bundle, signing assets, and metadata). When uploaded to App Store Connect, Apple performs App Thinning (such as App Slicing) to deliver only the assets and binaries required for the downloading device. Operationally, APKs and IPAs can be directly installed onto physical test devices (subject to signing and provisioning), whereas an AAB must first be converted into split APKs (e.g., using `bundletool`) prior to local device installation.
# 1. Standalone APK installs directly via ADB
adb install myapp-universal.apk
# 2. AAB requires bundletool to generate device-tailored split APKs
bundletool build-apks --bundle=myapp.aab --output=myapp.apks --connected-device
# 3. Install the generated split APK set onto the connected device
bundletool install-apks --apks=myapp.apks
10What protection does Transport Layer Security (TLS) provide for a mobile application's network calls, and how do modern mobile operating systems enforce secure transport defaults?
Transport Layer Security (TLS) protects mobile network calls by providing three essential guarantees: confidentiality (encrypting data in transit so eavesdroppers cannot read payloads or headers), data integrity (detecting any tampering or modification of requests and responses in transit), and server authentication (validating the server's digital certificate against trusted Certificate Authorities to prevent Man-in-the-Middle attacks). Modern mobile operating systems enforce secure transport by default by blocking unencrypted cleartext HTTP traffic: 1. iOS enforces App Transport Security (ATS), requiring network connections (such as those via URLSession) to use HTTPS with TLS 1.2+ unless domain exceptions are explicitly defined in Info.plist. 2. Android (API 28+) disables cleartext HTTP traffic by default (`cleartextTrafficPermitted=false`). Allowing unencrypted HTTP requires explicit exceptions via Network Security Configuration (`network_security_config.xml`) or the manifest attribute `android:usesCleartextTraffic`.
11How do you implement biometric authentication (Face ID / Fingerprint) with platform secure storage to ensure cryptographic keys are unlocked only upon successful biometric verification?
To securely gate cryptographic keys behind biometrics, an app must not rely merely on a boolean UI callback from a biometric prompt. Instead, cryptographic keys must be generated and stored inside hardware-backed storage (Secure Enclave on iOS, Android Keystore / StrongBox on Android) configured with access control policies that enforce biometric authentication prior to key usage. On iOS, keys or Keychain items are configured using `SecAccessControlCreateWithFlags` with flags like `.biometryCurrentSet` (or `.userPresence`). When the private key is requested for signing or decryption, the OS automatically prompts the user for Face ID / Touch ID. On Android, keys are generated using `KeyGenParameterSpec.Builder` with `.setUserAuthenticationRequired(true)`. A cryptographic operation (such as a `Cipher` or `Signature`) is initialized and passed as a `BiometricPrompt.CryptoObject` to `BiometricPrompt.authenticate()`. The key is only unlocked and usable within `onAuthenticationSucceeded` via that authenticated `CryptoObject`. Using `.biometryCurrentSet` (iOS) or `setInvalidatedByBiometricEnrollment(true)` (Android) ensures that if a new fingerprint or biometric is enrolled on the device, the existing keys become permanently invalid, mitigating unauthorized access if device passcode is compromised.
val keyGen = KeyGenerator.getInstance(KeyProperties.KEY_ALGORITHM_AES, "AndroidKeyStore")
val spec = KeyGenParameterSpec.Builder(
"biometric_auth_key",
KeyProperties.PURPOSE_ENCRYPT or KeyProperties.PURPOSE_DECRYPT
)
.setBlockModes(KeyProperties.BLOCK_MODE_GCM)
.setEncryptionPaddings(KeyProperties.ENCRYPTION_PADDING_NONE)
.setUserAuthenticationRequired(true)
.setInvalidatedByBiometricEnrollment(true)
.build()
keyGen.init(spec)
keyGen.generateKey()
12What caching strategies and pipeline optimizations can you implement to significantly reduce pull request build times in cross-platform mobile CI runners?
To drastically reduce pull request build times on mobile CI runners, optimizations must target dependency resolution, compilation caching, and selective execution. First, implement effective dependency caching keyed on lockfile hashes (e.g., `package-lock.json`, `Podfile.lock`, `gradle/verification-metadata.xml`, or SPM package resolved files). This avoids downloading or re-resolving external packages on clean runner instances. Second, configure native build caches. For Android, enable the Gradle Build Cache (`--build-cache`) with remote HTTP caching or CI-native persistent caching for `~/.gradle/caches` and build directories. For iOS, cache Xcode `DerivedData` and Swift Package / CocoaPods compilation artifacts carefully, or leverage modern remote caching tools like Tuist or Bazel. For React Native/Flutter, cache the JS bundle output, node_modules, and Flutter engine SDK cache. Third, apply selective job execution (change detection and test impact analysis). Use Git path filters to skip iOS builds when only Android or backend files changed, run fast lint and unit test stages before expensive UI tests or compilation steps, and avoid generating full release binaries (like AABs or universal IPAs) on PR runs where only a simulator/debug build or unit test run is necessary.
name: PR Fast Check
on:
pull_request:
paths:
- 'android/**'
- 'shared/**'
jobs:
android-unit-tests:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-java@v4
with:
distribution: 'zulu'
java-version: '17'
# Gradle build caching for dependencies & task outputs
- uses: gradle/actions/setup-gradle@v3
with:
cache-read-only: false
- name: Run Unit Tests & Lint Only
run: ./gradlew testDebugUnitTest lintDebug --build-cache --parallel
13How would you structure a shared feature so that presentation, domain use cases, and data access remain independently testable and reusable across iOS and Android?
To structure a shared feature for independent testability and reusability across iOS and Android, adopt a clean, layered architecture organized into Presentation, Domain, and Data layers: 1. **Domain Layer (Pure Shared Logic):** Contains pure business entities, repository interfaces (contracts), and use cases/interactors (e.g., `GetCartUseCase`, `ApplyPromoCodeUseCase`). This layer has zero dependencies on UI frameworks (UIKit, SwiftUI, Android Views, Jetpack Compose, Flutter widgets) or platform APIs. Each use case encapsulates a single business operation, making it 100% unit-testable with plain mocks. 2. **Data Layer (Data Access & Encapsulation):** Implements domain repository interfaces. It interacts with remote data sources (REST/GraphQL) and local persistence (SQLite/Key-Value). Network DTOs and database entities are strictly mapped to clean domain entities before leaving this layer, preventing external serialization schemas or database changes from leaking into domain or UI layers. 3. **Presentation Layer (UI & State Management):** Consists of state holders (ViewModels, Blocs, or Presenters) and UI views/widgets. The state holder calls domain use cases, manages UI state (loading, success, error), and exposes observable state to the view. The view simply observes this state and renders it. This structure allows unit testing use cases in isolation without UI, mocking repositories to test view models, and swapping presentation implementations between platforms while reusing the entire domain and data logic.
[ UI View / Compose / SwiftUI ]
|
v (Observes State / Dispatches Events)
[ ViewModel / Presenter / BLoC ] (Presentation Layer)
|
v (Executes pure business operations)
[ Use Case / Interactor ] (Domain Layer - Pure Shared Logic)
|
v (Calls interface contract)
[ Repository Interface ] (Domain Layer Contract)
^
| (Implements)
[ Repository Implementation ] (Data Layer)
|---- Maps NetworkDTO -> DomainEntity
|---- Maps DatabaseEntity -> DomainEntity
[ API Client / Local DB / Storage ]
14How do you evaluate when local component state is no longer sufficient and choose an appropriate architectural state management approach for a multi-developer cross-platform codebase?
In cross-platform mobile development, transitioning from local component state (`useState`/`StatefulWidget`) to an architectural state management solution is driven by state scope, lifecycle requirements, and testability: 1. **When Local State is Insufficient**: - **Cross-Component Sharing & Prop Drilling**: When state must be accessed or modified across disparate navigation routes or distant branches of the widget/component tree. - **Lifecycle Persistence**: When data must survive screen teardowns, route transitions, or navigation resets (e.g., user session, cart contents, cached feeds). - **Separation of Concerns & Testability**: When business logic, validation, and side effects become entangled with UI components, making automated headless unit testing difficult. 2. **Choosing an Architecture for Multi-Developer Codebases**: - **Unidirectional Data Flow / Predictable Containers (e.g., BLoC, Redux, Riverpod)**: Enforces strict separation where UI dispatches explicit events/actions and renders immutable state emitted by dedicated business logic units. This creates clear contracts, reduces side effects, and enables independent unit testing of business logic. - **Atomic / Scoped Reactive Stores (e.g., Zustand, MobX, Provider)**: Offers lower boilerplate and flexible selector subscriptions, suitable when teams want lightweight decoupling and targeted component re-renders. In a team setting, the primary architectural goal is isolating pure business logic from UI rendering layers and using selective subscriptions to prevent unnecessary full-tree re-renders.
15How would you investigate and reduce jank in a cross-platform mobile app screen that stutters while scrolling a long list of images and dynamic content?
I would first reproduce the stutter on real devices and profile it, because scrolling jank can come from several places: missed frame budget, JS or Dart work, main/UI-thread work, GPU/raster work, layout, image decoding, memory pressure, or network loading. At 60 Hz the app has about 16.7 ms per frame, and at 120 Hz about 8.3 ms, so expensive rendering, decoding, layout, or synchronous computation during scrolling can cause dropped frames. I would use tools such as Flutter DevTools, React Native performance tools or Flipper, Android Studio Profiler, Xcode Instruments, and frame timeline views to identify the real bottleneck. For the long list, I would make sure it is lazy or virtualized: for example, FlatList, FlashList, or RecyclerListView in React Native, or ListView.builder, SliverList, or similar builders in Flutter. I would use stable keys, avoid rebuilding or re-rendering every row when parent state changes, memoize row components or selectors where appropriate, keep build/renderItem cheap, and move sorting, filtering, JSON parsing, formatting, or image processing out of the scroll path. If item sizes are predictable, I would provide layout hints such as getItemLayout in React Native or fixed/prototype item extents in Flutter. For images, I would serve appropriately sized thumbnails, cache them, avoid decoding full-resolution images for small cells, use placeholders and lazy loading, and watch memory churn from too many large bitmaps. I would also simplify overly complex row layouts, reduce expensive shadows/clipping/overdraw/opacity where relevant, batch or paginate data, and then re-profile to confirm that dropped frames and frame times improved.
1. Record a trace while scrolling on a real device.
2. Check frame timeline: are frames exceeding 16.7 ms / 8.3 ms?
3. Identify bottleneck: JS/Dart, main/UI thread, raster/GPU, image decode, memory, or network.
4. Fix the largest measured bottleneck: virtualization, image resizing/caching, row memoization, layout simplification, moving work off scroll path.
5. Re-test on low-end and target-refresh-rate devices.
16How do thread boundaries operate across native bridge calls, and how do you prevent thread-hopping stalls or UI jank when native modules perform heavy background work?
Cross-platform frameworks use specific threading conventions for bridge calls: for instance, standard Flutter `MethodChannel` calls arrive on the platform's main UI thread, while traditional React Native bridge calls run on a dedicated JavaScript thread and dispatch asynchronously to native threads. When a native bridge method executes on the native UI/main thread, running CPU-heavy operations, long synchronous disk I/O, or blocking network tasks blocks the main run loop. This leads to dropped frames, visible UI jank, and on Android, Application Not Responding (ANR) errors or iOS watchdog termination. To prevent this, native bridge handlers should offload heavy or blocking work to background execution pools (e.g., Kotlin Coroutines with `Dispatchers.IO`/`Dispatchers.Default`, Android `ThreadPoolExecutor`, or Swift `Task.detached` / GCD `DispatchQueue.global()`). Once the work finishes, the result must be dispatched back to the bridge thread expected by the framework (e.g., returning results on the UI thread for standard MethodChannels or using background task queues), ensuring cross-platform state updates do not block the UI rendering cycle.
class ImageProcessorPlugin : MethodChannel.MethodCallHandler {
private val scope = CoroutineScope(Dispatchers.Default + SupervisorJob())
override fun onMethodCall(call: MethodCall, result: MethodChannel.Result) {
if (call.method == "applyFilter") {
val imageBytes = call.argument<ByteArray>("image") ?: return result.error("INVALID_ARG", "Image null", null)
// Dispatch heavy computation off the main UI thread
scope.launch {
try {
val processed = processBitmapBytes(imageBytes) // CPU heavy work
withContext(Dispatchers.Main) {
result.success(processed)
}
} catch (e: Exception) {
withContext(Dispatchers.Main) {
result.error("PROCESSING_FAILED", e.localizedMessage, null)
}
}
}
} else {
result.notImplemented()
}
}
}
17How would you design a caching and invalidation strategy for list and feed screens using Stale-While-Revalidate and conditional HTTP headers?
A resilient caching and invalidation strategy for feed and list screens combines the Stale-While-Revalidate (SWR) pattern with conditional HTTP validation headers (such as `ETag` / `If-None-Match` or `Last-Modified` / `If-Modified-Since`). When the screen opens, it immediately reads and renders cached data from a local database (the Single Source of Truth, e.g., Room or Core Data/SQLite), giving the user an instant UI render. Simultaneously, a background network request is dispatched with the cached `If-None-Match: <etag>` header. If the server responds with `304 Not Modified`, no payload is transferred, validating the local cache while saving bandwidth and battery. If the server responds with `200 OK`, the new data and updated `ETag` are written to the local database in a transaction, and reactive database observers automatically emit the updated list to the UI. For pagination, page tokens or offsets are stored alongside cached records. Invalidation is triggered by TTL expiration, user actions (such as pull-to-refresh bypassing conditional headers), or local mutation side-effects (e.g., creating or deleting an item locally updates the database optimistically and flags cached page cursors for revalidation).
fun getFeed(): Flow<List<FeedItem>> = flow {
val cached = feedDao.getFeedItems()
if (cached.isNotEmpty()) emit(cached)
val lastEtag = feedDao.getFeedEtag()
try {
val response = api.fetchFeed(ifNoneMatch = lastEtag)
if (response.code() == 200 && response.body() != null) {
feedDao.updateFeedTransaction(response.body()!!, response.headers()["ETag"])
emit(feedDao.getFeedItems())
}
} catch (e: Exception) {
if (cached.isEmpty()) throw e
}
}
18A financial mobile application must store refresh tokens securely on device, support biometric unlocking, survive OS upgrades, and operate securely during temporary offline sessions. How would you design this token storage and access architecture?
A robust mobile token storage architecture uses hardware-backed security modules: the iOS Keychain backed by the Secure Enclave and the Android Keystore backed by StrongBox or a Trusted Execution Environment (TEE). Refresh tokens must be encrypted at rest using keys protected by biometric cryptographic gating (such as `kSecAccessControlBiometryCurrentSet` or `kSecAttrAccessibleAfterFirstUnlockThisDeviceOnly` on iOS, and `setUserAuthenticationRequired(true)` with `AUTH_BIOMETRIC_STRONG` on Android). Cryptographic gating ensures that the cryptographic key is only unwrapped by the hardware upon successful biometric authentication, rather than relying on a bypassable boolean check in application code. To survive OS upgrades while preventing unauthorized access, keys bind to device hardware while enforcing enrollment change policies that invalidate or prompt re-authentication if new biometrics are registered (e.g., `setInvalidatedByBiometricEnrollment(true)` on Android). For temporary offline sessions, short-lived encrypted access tokens and restricted local offline session states can operate within defined local TTLs and scoped privileges, while deferring privileged refresh actions until network connectivity returns. Upon reconnection, refresh token rotation with single-use replay detection on the backend validates the session; if token reuse or account revocation is detected, the backend returns an invalidation signal that triggers the client to purge hardware-backed keys and clear offline storage.
val keyStore = KeyStore.getInstance("AndroidKeyStore").apply { load(null) }
val keyGenerator = KeyGenerator.getInstance(KeyProperties.KEY_ALGORITHM_AES, "AndroidKeyStore")
val spec = KeyGenParameterSpec.Builder(
"refreshTokenKeyAlias",
KeyProperties.PURPOSE_ENCRYPT or KeyProperties.PURPOSE_DECRYPT
)
.setBlockModes(KeyProperties.BLOCK_MODE_GCM)
.setEncryptionPaddings(KeyProperties.ENCRYPTION_PADDING_NONE)
.setUserAuthenticationRequired(true)
.setUserAuthenticationParameters(0, KeyProperties.AUTH_BIOMETRIC_STRONG)
.setInvalidatedByBiometricEnrollment(true)
.build()
keyGenerator.init(spec)
keyGenerator.generateKey()
19How would you design a scalable CI/CD infrastructure for a cross-platform mobile monorepo supporting multiple teams while managing limited and costly macOS runner capacity?
To design a scalable CI/CD infrastructure for a cross-platform mobile monorepo while optimizing scarce and expensive macOS capacity, the architecture must rely on graph-aware affected build detection, strict workload bifurcation across hybrid runner fleets, and virtualization/ephemeral orchestration for macOS. First, implement a monorepo build graph tool (such as Bazel, Nx, or Turborepo) with distributed remote caching. On every pull request, the CI pipeline calculates the directed acyclic graph (DAG) diff against the target branch to build and test only affected packages and their downstream dependents, bypassing unchanged modules entirely. Second, implement a hybrid runner allocation strategy: offload all tasks that do not strictly require Xcode (such as TypeScript/Dart linting and compilation, unit tests, static code analysis, security scanning, and Android Gradle builds) to cost-effective, scalable Linux/Kubernetes runners. Reserve macOS runners strictly for final iOS assembly, Swift/Objective-C compilation, code signing, and iOS Simulator test execution. Third, manage macOS runners using virtualization infrastructure (such as Tart, Anka, or AWS/MacStadium bare-metal nodes orchestrated via Nomad or Kubernetes). Each iOS build runs in a clean, ephemeral virtual machine with pre-warmed toolchains and derived-data caches, preventing runner drift and enabling rapid autoscaling based on pipeline queue depth.
20How would you design a cross-platform mobile architecture for a product expected to share most business logic across iOS and Android while still allowing platform-specific UI, navigation, and native integrations to evolve independently?
I would design the app around a shared core that owns stable business behavior: domain models, validation, use cases, business rules, and data-access contracts. Platform-specific UI, navigation, native SDKs, permissions, lifecycle handling, and app-shell concerns should stay outside that core. The shared code should not import UIKit, SwiftUI, Jetpack, Android framework APIs, React Native navigation, Flutter navigation, or native SDK details. Instead, it should depend on narrow interfaces such as AuthRepository, SecureStorage, Analytics, CameraService, or PaymentProvider that each platform implements. I would organize code by feature as much as possible rather than creating one large shared layer. For example, checkout, search, account, and messaging can each have shared domain/use-case code, state contracts, and repository interfaces. The iOS and Android shells then own screen composition, native UI patterns, navigation stacks, permissions, and adapter wiring. Presentation logic can be shared when it is truly UI-framework-agnostic, such as reducers, state machines, or view-model contracts that emit simple state and actions, but actual views and navigation should remain platform-owned so each platform can evolve independently. The main trade-off is to share common, stable business logic aggressively while avoiding abstractions that hide real platform differences. I would enforce boundaries with module dependency rules, dependency injection, public APIs, contract tests, architecture checks, and clear ownership. Native integrations should use ports/adapters so the shared feature code sees a common capability while each platform handles SDK behavior, lifecycle, permissions, errors, and UX differences in its own layer.