Skip to content
RED SAGSkills

Mobile App Developer interview questions and answers

30 questions interviewers really ask, with what a strong answer covers. Read them, then test yourself in a timed mock interview.

Start a Mobile App Developer mock interview

Easy questions easy

1. What is the difference between native and cross-platform mobile development? When would you choose each?

A strong answer covers:

  • Native: Kotlin/Swift with platform SDKs, one codebase per platform
  • Cross-platform: Flutter or React Native, shared code for both
  • Native gives best access to new OS features and performance
  • Cross-platform saves time and team size
  • Choice depends on team skills, budget, performance and platform features
Show model answer

Native development means building the Android app in Kotlin with Jetpack Compose and the iOS app in Swift with SwiftUI, using each platform SDK directly. You get the best performance, the newest OS features on day one, and the most natural look and feel, but you maintain two codebases and often two teams. Cross-platform frameworks like Flutter and React Native let you share most of the code between platforms, so a smaller team can ship faster. They are a great fit for typical business apps like e-commerce or booking. I would choose native for apps that depend heavily on platform features, advanced graphics, or very tight performance, and cross-platform when time to market and team size matter more. Kotlin Multiplatform is a middle option that shares business logic but keeps native UI.

2. Explain the lifecycle of a mobile app screen. Why does it matter?

A strong answer covers:

  • Screens move through created, started/visible, resumed/active, paused, stopped, destroyed
  • Release resources like camera or sensors when paused or stopped
  • The OS can kill a backgrounded app
  • Save state so the user can continue where they left off
  • Start and stop observing data at the right time
Show model answer

A screen goes through states as the user uses the app. On Android, an Activity goes through onCreate, onStart and onResume when it opens, and onPause, onStop and onDestroy when it leaves. iOS and SwiftUI have similar events like onAppear and scene phases for active, inactive and background. It matters because phones have limited resources. I must stop things like the camera, location updates or video when the screen is not visible, otherwise I waste battery. The OS can also kill the app while it is in the background, so I save important state, like form input, and restore it when the user returns. Lifecycle-aware tools, like collecting flows with repeatOnLifecycle, help do this correctly.

3. How do you handle network calls and errors in a mobile app?

A strong answer covers:

  • Run calls off the main thread with async/await or coroutines
  • Show loading, success, empty and error states
  • Handle no network, timeouts and server errors differently
  • Retry with backoff where safe; let the user retry
  • Cache data so the app is useful offline
Show model answer

I always run network calls off the main thread, using coroutines with Retrofit or Ktor on Android, async/await with URLSession on iOS, or async functions in Flutter and React Native. Each screen has clear UI states: loading, success, empty and error. I treat errors differently: no internet shows a friendly offline message, a timeout offers a retry, a 401 sends the user to login, and a 5xx shows a generic error and is logged to monitoring. For safe requests I retry automatically with exponential backoff. I set sensible timeouts, and I cache the last good data locally so the screen still shows something when offline. I never show raw error messages or stack traces to users.

4. What is MVVM and why is it used in mobile apps?

A strong answer covers:

  • Model: data and business logic
  • View: UI that displays state and sends user events
  • ViewModel: holds UI state and handles events, no direct UI references
  • Easier testing and separation of concerns
  • Works well with reactive state like StateFlow or Observable
Show model answer

MVVM stands for Model, View and ViewModel. The Model is the data layer, such as repositories that talk to the network and local database. The View is the UI, a Compose screen or SwiftUI view, which only displays state and forwards user actions. The ViewModel sits in between: it holds the screen state, handles events like button taps, calls the repository, and exposes the result as observable state, such as a StateFlow or an @Observable object. The ViewModel never holds a reference to the View. This separation keeps UI code simple, makes business logic easy to unit test without a device, and on Android the ViewModel also survives rotation. Many teams combine it with unidirectional data flow, where state goes down and events go up.

5. How do push notifications work in a mobile app?

A strong answer covers:

  • App registers with FCM or APNs and gets a device token
  • Token is sent to and stored by your backend
  • Backend sends a message to FCM/APNs, which delivers it to the device
  • User must grant notification permission
  • Handle token refresh and tapping a notification to open the right screen
Show model answer

Push notifications go through the platform services: Firebase Cloud Messaging for Android and Apple Push Notification service for iOS. When the app starts, it asks the user for notification permission, which is required on iOS and on Android 13 and later. It then registers with FCM or APNs and receives a device token. The app sends this token to our backend, linked to the user. When we want to notify someone, the backend sends a request with the token and payload to FCM or APNs, and they deliver it to the device even if the app is closed. In the app I handle token refresh, show the notification correctly in foreground, and when the user taps it I deep link to the right screen.

6. How do you run slow work like network calls or database reads in a mobile app without freezing the UI?

A strong answer covers:

  • The main thread draws the UI and handles touches, so it must not be blocked
  • Use the platform async tools: Kotlin coroutines, Swift async/await, Dart Futures or JS promises
  • Move blocking or heavy work to a background dispatcher, actor or isolate
  • Update the UI back on the main thread
  • Tie work to a lifecycle scope so it is cancelled when the screen goes away
Show model answer

The main thread draws every frame and handles touches, so anything slow there causes jank or an ANR on Android. I use the async tools of the platform. In Kotlin I launch coroutines in viewModelScope and wrap blocking I/O in withContext(Dispatchers.IO). In Swift I use async/await, keep UI code on the main actor and let URLSession suspend instead of blocking. In Flutter, network calls are Futures, and heavy CPU work like parsing huge JSON goes to another isolate with compute or Isolate.run. In React Native I keep the JS thread free of long loops. I also tie the work to a lifecycle scope, so if the user leaves the screen the work is cancelled and nobody updates a dead view. Finally, I show loading and error states so the user always knows what is happening.

7. What options do you have for storing data on the device, and how do you choose between them?

A strong answer covers:

  • Key-value stores (DataStore, UserDefaults) for small settings
  • A database (Room, SwiftData, Core Data, SQLite) for structured, queryable data
  • Files for large blobs like images or downloads
  • Keychain or Keystore for secrets such as tokens
  • Consider size, querying needs, security and migrations
Show model answer

I pick storage by the shape and sensitivity of the data. Small settings like theme or onboarding done go in a key-value store: DataStore on Android, UserDefaults on iOS, shared_preferences in Flutter. Structured data that I need to query, sort or observe, like a list of orders, goes in a database such as Room, SwiftData, Core Data or SQLite through a library like Drift. Large blobs like images, PDFs or downloaded media go into files in the app sandbox, often in the cache directory if they can be re-downloaded. Secrets such as refresh tokens go in the Keychain or in storage encrypted with the Android Keystore, never in plain preferences. I also plan for schema migrations from day one and avoid storing more personal data than the feature needs.

8. How do you request runtime permissions like camera or location, and what do you do when the user says no?

A strong answer covers:

  • Ask in context, at the moment the feature is used
  • Explain the benefit first with a short pre-permission screen
  • Declare purpose strings or manifest permissions
  • Handle denial: keep the app usable and offer an alternative
  • Guide to system settings when permanently denied
Show model answer

I ask only when the user starts the feature that needs it, for example when they tap Scan, not at first launch. Before the system dialog, I show a short screen explaining why, because the system dialog can usually be shown only once or twice. I make sure the manifest entry or the Info.plist purpose string is there, since a missing purpose string crashes on iOS. If the user denies, the app must still work: I hide or disable the feature, or offer another way, like typing a code instead of scanning. If they denied permanently, I show a gentle message with a button that opens the app settings page. I also re-check the permission each time, because users can revoke it in settings at any moment.

9. How do you make your app look good on different screen sizes, orientations and font sizes?

A strong answer covers:

  • Use flexible layouts instead of fixed sizes
  • Use density-independent units and scalable text (sp, Dynamic Type)
  • Adapt layouts using window size classes or breakpoints for tablets and foldables
  • Respect safe areas, notches and system bars
  • Test on small, large and rotated screens with large fonts
Show model answer

I start with flexible layouts: rows, columns and constraints that fill available space instead of fixed pixel widths. Sizes use dp or points, and text uses sp on Android and Dynamic Type on iOS so it scales with the user font setting, which means layouts must allow text to wrap. For tablets and foldables I use window size classes or breakpoints to switch layouts, for example a list and detail side by side on wide screens. I respect safe areas and insets so content is not hidden behind the notch, home indicator or keyboard. Then I test on a small phone, a large phone, a tablet and landscape, plus the largest font size, using previews in Compose or SwiftUI and real devices for the final check.

10. What is the difference between declarative UI (Compose, SwiftUI, Flutter, React Native) and the older imperative style?

A strong answer covers:

  • Imperative UI means creating views and changing them step by step
  • Declarative UI describes what the screen looks like for a given state
  • The framework rebuilds or recomposes when state changes
  • State should be the single source of truth, which reduces sync bugs
  • Needs care with state ownership and avoiding unnecessary rebuilds
Show model answer

In the imperative style, like Android XML views or UIKit, you build views once and then mutate them by hand: find the label, set its text, hide the spinner. It is easy to forget a step and leave the screen out of sync with the data. In declarative frameworks like Jetpack Compose, SwiftUI, Flutter and React Native, you write a function that describes the UI for the current state. When state changes, the framework re-runs that description and updates only what changed, through recomposition or diffing. This makes state the single source of truth and removes a whole class of sync bugs. The trade-off is that you must think about who owns each piece of state, and avoid expensive work or unstable inputs that cause too many rebuilds.

Medium questions medium

1. How would you add offline support to an app that currently only works online?

A strong answer covers:

  • Local database as the source of truth (Room, SwiftData, SQLite)
  • Repository syncs between network and local store
  • Queue writes made offline and send them later with background work
  • Handle conflicts and failed syncs
  • Show users sync status and stale data clearly
Show model answer

I would move to an offline-first design. First I add a local database such as Room on Android or SwiftData on iOS, and make it the single source of truth: the UI observes the database, not the API. A repository fetches from the network when possible and writes results into the database, so screens update automatically. For writes, I save the change locally right away, mark it as pending, and put it in a sync queue. A background job, like WorkManager or BGTaskScheduler, sends pending changes when the network is back, with retries. On the server side I need version numbers to detect conflicts and a clear merge rule. In the UI I show when data was last updated and which items are not synced yet.

2. Users say a list in your app stutters while scrolling. How would you find and fix the problem?

A strong answer covers:

  • Reproduce on a low-end device in release mode
  • Profile with Android Studio Profiler, Xcode Instruments or Flutter DevTools
  • Look for main-thread work: image decoding, parsing, database calls
  • Use lazy lists with stable keys and avoid unnecessary recomposition or re-render
  • Downsample and cache images, flatten layouts
Show model answer

First I reproduce it on a real low-end device using a release build, because debug builds are slower and misleading. Then I profile: Android Studio Profiler and system traces, Xcode Instruments Time Profiler, or Flutter DevTools frame chart. I look for frames that miss the 16ms budget (or 8ms at 120Hz) and what runs on the main thread during them. Common causes are decoding large images, JSON parsing or database queries on the main thread, deep or complex item layouts, and items re-rendering when nothing changed. Fixes include moving work to background threads, using an image library that downsamples and caches, giving list items stable keys, making item parameters stable or memoised, and simplifying layouts. Then I measure again to confirm.

3. Walk me through how you release a new version of a mobile app to the stores.

A strong answer covers:

  • Version code/build number and version name
  • Signed release builds via CI (Fastlane, Gradle, Xcode Cloud)
  • Internal and beta testing (TestFlight, Play testing tracks)
  • Store review, release notes and screenshots
  • Staged rollout with crash and metric monitoring
  • Feature flags and forced update strategy
Show model answer

I start by bumping the version name and the build number, which must always increase. CI builds a signed release, for example an Android App Bundle with Play App Signing and an iOS archive, usually automated with Fastlane or Xcode Cloud. The build first goes to internal testers and QA through Play internal testing and TestFlight, then to a wider beta group. After sign-off, I submit to the stores with release notes and any new screenshots, and wait for review. On release I use a staged rollout, starting with a small percentage, and watch Crashlytics, ANR rates and key business metrics. If something goes wrong I halt the rollout and use feature flags to disable the feature while a hotfix is prepared. We also keep a minimum supported version check for critical fixes.

4. What is dependency injection and how have you used it in a mobile app?

A strong answer covers:

  • Objects receive dependencies instead of creating them
  • Makes testing easy by swapping fakes
  • Tools: Hilt/Dagger, Koin, Swift initializer injection, get_it or Riverpod
  • Scopes like singleton, per screen
  • Avoid service locator overuse and hidden globals
Show model answer

Dependency injection means a class gets the objects it needs from outside, usually through its constructor, instead of creating them itself. For example, a ProfileViewModel receives a UserRepository rather than building its own Retrofit client. This makes the code loosely coupled and easy to test, because in a unit test I can pass a fake repository. On Android I have used Hilt, which generates the wiring at compile time and handles scopes such as singleton for the database and ViewModel scope for screen objects. In Swift I usually do plain initializer injection with protocols, and in Flutter I have used Riverpod. I try to avoid hidden global singletons, because they make tests depend on each other and hide what a class really needs.

5. What security practices do you follow when building a mobile app?

A strong answer covers:

  • Store secrets in Keychain or Keystore, never hard-code API secrets
  • Use HTTPS only, consider pinning for high-risk apps
  • Short-lived tokens with refresh, secure logout
  • Validate everything on the server; the client can be modified
  • Obfuscation, root/jailbreak checks and Play Integrity / App Attest where needed
  • Minimal permissions and careful logging
Show model answer

I assume the app binary can be decompiled and the device can be rooted, so I never put real secrets in the app. Tokens are stored in the iOS Keychain or Android Keystore-backed storage. All traffic uses HTTPS, and for high-risk apps like banking I add certificate or public key pinning with backup pins. I use short-lived access tokens with refresh tokens and clear everything on logout. Most importantly, the server checks every permission and input, because the client can be modified. For extra protection I enable R8 obfuscation, use Play Integrity or App Attest to check the app is genuine, and avoid logging personal data. I also request only the permissions the app really needs and follow OWASP MASVS as a checklist.

6. How do you manage state in a Flutter or React Native app as it grows beyond a few screens?

A strong answer covers:

  • Separate local UI state from shared app or server state
  • Use a state library (Riverpod, Bloc, Redux Toolkit, Zustand) for shared state
  • Use a server-state cache (TanStack Query) for API data
  • Unidirectional data flow and immutable state
  • Keep business logic out of widgets or components so it is testable
Show model answer

I first split state into types. Local UI state, like whether a dropdown is open, stays in the widget or component with setState or useState. Shared app state, like the logged-in user or cart, goes in a dedicated layer: in Flutter I usually use Riverpod or Bloc, and in React Native Zustand or Redux Toolkit. Server data is really a cache, so in React Native I use TanStack Query, which handles loading, caching, refetching and invalidation, and in Flutter a repository with a provider that exposes async values. I keep data flowing one way: the UI dispatches events, logic updates immutable state, and the UI rebuilds. Business logic lives outside widgets so it can be unit tested. I also scope providers or selectors carefully so a change only rebuilds the parts of the screen that use it.

7. What testing strategy do you use for a mobile app, and what do you test at each level?

A strong answer covers:

  • Many fast unit tests for ViewModels, use cases and mappers
  • Integration tests for repositories, database and network with fakes
  • A smaller set of UI tests for key user journeys
  • Snapshot or screenshot tests for visual regressions
  • Run on CI, plus testing on real devices or device farms
Show model answer

I follow the test pyramid. Most tests are fast unit tests for ViewModels, use cases, mappers and validation, with fake repositories and a test dispatcher so they are deterministic. Next are integration tests: Room or SQLite migrations, repository logic with a mock web server, and serialization of real API samples. On top is a small set of UI tests for critical journeys like login, checkout and onboarding, written with Compose testing or Espresso on Android, XCUITest on iOS, or Maestro or Detox for cross-platform apps, all against a fake backend. Screenshot tests catch visual regressions across themes and font sizes. Everything runs on CI for each pull request, and before release we run the UI suite on a device farm covering low-end devices and older OS versions.

8. How would you implement deep links in a mobile app from end to end?

A strong answer covers:

  • Use verified https links (App Links, Universal Links) with the association files on the domain
  • Central router maps URLs to destinations
  • Validate parameters and treat links as untrusted input
  • Handle cold start, warm start, and logged-out users
  • Fallback to the website when the app is not installed, and test every route
Show model answer

I prefer verified https links: Android App Links with assetlinks.json and iOS Universal Links with the apple-app-site-association file on our domain, so the same URL opens the app or falls back to the website. In the app, one central router parses the URL into a typed destination, so screens never parse raw strings. I validate every parameter because links are untrusted input that anyone can craft. The router must handle cold start, when the app launches from the link, and warm start, when it is already running. If the destination needs login, I store it as pending and continue after sign-in. I also build a sensible back stack so Back goes somewhere useful. Finally, I add automated tests for each route and check links with adb and on real devices.

9. How do you make a mobile app accessible, and how do you check it?

A strong answer covers:

  • Labels and roles for screen readers (TalkBack, VoiceOver)
  • Support dynamic font sizes without clipping
  • Sufficient color contrast and not relying on color alone
  • Large touch targets and logical focus order
  • Test with real assistive tools and automated checks
Show model answer

I build accessibility in from the start. Every interactive element gets a clear label and role, for example contentDescription or semantics in Compose and accessibilityLabel in SwiftUI or React Native, and decorative images are hidden from screen readers. Text uses scalable sizes so it follows the user font setting, and layouts wrap instead of clipping at the largest sizes. Colors meet contrast guidelines, and I never use color alone to show an error. Touch targets are at least 48 dp or 44 points, and related items are grouped so TalkBack and VoiceOver read them in a sensible order. To check, I use Accessibility Scanner and Xcode Accessibility Inspector, add automated checks in UI tests, and regularly walk through key flows with the screen reader turned on and the font at maximum size.

10. How would you set up CI/CD for a mobile app?

A strong answer covers:

  • Run lint, unit tests and builds on every pull request
  • Manage signing keys and certificates securely in CI
  • Automate version numbers and builds with Fastlane or Gradle tasks
  • Distribute internal builds to testers (TestFlight, Play internal testing)
  • Automate store upload, staged rollout and symbol upload
Show model answer

On every pull request, CI runs lint, static analysis, unit tests and a debug build for both platforms, and blocks merge on failures. For releases, a pipeline in GitHub Actions, Bitrise or similar builds signed release artifacts. Signing material never lives in the repo: the Android keystore and passwords are encrypted CI secrets, and iOS certificates and profiles come from fastlane match or App Store Connect API keys. Build numbers are generated automatically so every upload is unique. Fastlane lanes upload builds to TestFlight and Play internal testing so QA always has the latest version. The same lanes upload dSYMs and R8 mapping files to Crashlytics. Promotion to production is a manual approval followed by a staged rollout, and we watch crash-free rate before going to 100 percent.

Hard questions hard

1. How would you design the mobile client for a chat app like WhatsApp?

A strong answer covers:

  • Local database for messages, UI observes it
  • Real-time connection (WebSocket) while in foreground, push when in background
  • Message states: sending, sent, delivered, read; client-generated IDs
  • Outgoing queue with retries and ordering
  • Paging of history, media upload/download handling
  • Battery, data usage and end-to-end encryption concerns
Show model answer

The core is a local database of conversations and messages, and the UI only observes it. When the app is in the foreground, I keep a WebSocket connection for real-time messages, typing and receipts, with reconnect and backoff. In the background the OS limits sockets, so new messages arrive through push notifications, which can wake the app to fetch. When the user sends a message, I create it locally with a client-generated ID and status "sending", show it immediately, and put it in an outgoing queue. The server acknowledges with the same ID so retries never duplicate. Message history loads with paging. Media is uploaded separately with resumable uploads and thumbnails. I would also consider end-to-end encryption with keys in secure storage, and limit battery and data use.

2. Your app has grown to 40 developers and one huge module. Builds are slow and teams block each other. How would you restructure it?

A strong answer covers:

  • Split into feature modules and core/shared modules
  • Clear dependency rules; features do not depend on each other directly
  • Navigation and communication through interfaces or a router
  • Build speed: caching, parallel builds, only rebuild changed modules
  • Code ownership and gradual migration
Show model answer

I would split the app into modules: core modules for networking, design system, analytics and data, and feature modules for areas like login, search and payments. A strict rule is that feature modules do not depend on each other; they talk through interfaces in an API module or through a navigation router, so teams can work independently. On Android this means Gradle modules with convention plugins, and on iOS Swift packages. Smaller modules let Gradle and Xcode build in parallel and reuse build caches, so a change in one feature does not rebuild everything. I would add code owners for each module and dependency checks in CI. The migration should be gradual: extract the design system and core first, then move one feature at a time while measuring build time.

3. A delivery app must track the rider location all day. How do you balance accuracy with battery life and OS limits?

A strong answer covers:

  • Use fused/significant location APIs with suitable accuracy and intervals
  • Adapt frequency to movement and app state
  • Foreground service on Android, background location mode on iOS
  • Batch uploads and avoid waking the radio often
  • Clear permission flow and user transparency
  • Handle OS restrictions and doze mode
Show model answer

GPS and the network radio are the biggest battery costs, so I avoid using them at full power all the time. On Android I use the Fused Location Provider and on iOS Core Location, choosing accuracy and interval based on need: high accuracy during an active delivery, lower when the rider is idle. I adapt using motion detection, so when the rider stops, updates slow down. Android requires a foreground service with a visible notification for continuous tracking, and iOS needs the background location mode and Always permission, which must be justified clearly to the user. Instead of sending each point immediately, I store points locally and upload them in batches, which lets the radio sleep. I also test under doze and low power mode, and monitor battery impact in production.

4. In a Flutter or React Native app, you need a feature that only exists in native SDKs, such as a payment terminal SDK. How would you integrate it well?

A strong answer covers:

  • Write a native module or plugin (platform channels, Pigeon, TurboModules/Expo modules)
  • Design a small, typed, async API surface
  • Handle threading and lifecycle on the native side
  • Map native errors to clear errors in Dart/JS
  • Test on both platforms and keep versions in sync
Show model answer

I would wrap the native SDK in a plugin with a small, focused API. In Flutter I would use platform channels, ideally with Pigeon to generate type-safe code on both sides. In React Native I would write a TurboModule or an Expo module, which works with the New Architecture and gives typed bindings. The API should be async and high level, like startPayment(amount) returning a result, instead of exposing every SDK method. On the native side I handle threading, because many SDKs call back on background threads, and lifecycle events like the app going to background. Native errors are mapped to clear error codes in Dart or JavaScript. I keep the Android and iOS implementations behaving the same, write tests on both, and document which SDK versions are supported.

5. How would you measure and improve app performance for real users in production, not just on your test phone?

A strong answer covers:

  • Track startup time, frame drops, ANRs, crashes and network latency
  • Use tools like Firebase Performance, Play vitals, MetricKit, Sentry
  • Segment by device class, OS version and country/network
  • Set budgets and alert on regressions per release
  • Benchmarks in CI (Macrobenchmark, XCTest metrics)
Show model answer

Test phones are usually fast and on good Wi-Fi, so I rely on production data. I track cold and warm startup time, slow and frozen frames, ANRs, crash-free users, app size and network request latency. Sources include Android vitals in Play Console, Apple MetricKit and Xcode Organizer, plus a tool like Firebase Performance or Sentry for custom traces such as time to load the home feed. I always look at percentiles like p90 and segment by device class, OS version and network type, because low-end Android phones on slow networks often show the real problems. Each release is compared to the previous one, with alerts on regressions. In CI I run Macrobenchmark or XCTest performance tests so regressions are caught before release, and I set clear budgets for startup and app size.

6. Users keep running old versions of your app for months. How do you evolve the API and decide when to force an update?

A strong answer covers:

  • Assume old clients exist; make API changes backward compatible
  • Version the API or use additive changes and tolerant parsing
  • Send the app version with each request and monitor version adoption
  • Remote config for soft and hard minimum versions
  • Force updates only for security or breaking reasons, with a clear in-app flow
Show model answer

Unlike the web, I cannot redeploy every client, so every API change must assume old app versions are still out there. I prefer additive changes: new optional fields and new endpoints, never renaming or removing fields that old apps parse. Clients parse tolerantly, ignoring unknown fields and handling unknown enum values with a default. When a breaking change is unavoidable, I version the endpoint and keep the old one until usage drops. Each request sends the app version, so we can see adoption per version and remove dead paths with data. I keep two remote values: a soft minimum that shows a dismissible update prompt, and a hard minimum that blocks use, which I reserve for security issues or truly broken versions. On Android I use the Play In-App Updates API, and on iOS a screen linking to the App Store.

7. Your crash reports show rising out-of-memory crashes after a release. How do you find and fix the problem?

A strong answer covers:

  • Use crash data to narrow the version, devices and user flows
  • Reproduce the flow and profile memory over repeated navigation
  • Use LeakCanary, Android Studio Profiler, Xcode Memory Graph or Instruments
  • Common causes: retained contexts, retain cycles, listeners, large bitmaps, caches
  • Fix, add tests or checks, and verify with staged rollout metrics
Show model answer

First I narrow it down with data: which version introduced it, which devices, and what breadcrumbs or screens appear before the crash. Rising OOMs after many minutes usually mean a leak, while instant OOMs on one screen point to huge allocations like full-size bitmaps. Then I reproduce the flow, for example opening and closing the product screen twenty times, while watching memory in Android Studio Profiler or Xcode Instruments. If memory climbs and never returns, I take a heap dump or use the Xcode Memory Graph to see which objects are retained and by what. LeakCanary helps catch Android leaks automatically in debug builds. Typical causes are closures capturing self strongly, unregistered listeners, static references to an Activity, unbounded caches and undecoded large images. After fixing, I watch the OOM rate in the staged rollout before releasing to everyone.

8. How would you implement secure login with an OAuth or OpenID Connect provider in a mobile app?

A strong answer covers:

  • Use the authorization code flow with PKCE; no client secret in the app
  • Use the system browser (ASWebAuthenticationSession, Custom Tabs), not a WebView
  • Validate state and use verified redirect URIs
  • Store tokens securely and use short-lived access tokens with refresh
  • Handle refresh races, revocation and logout
Show model answer

A mobile app is a public client, so it cannot keep a client secret. I use the authorization code flow with PKCE: the app creates a random code verifier, sends its hash with the authorization request, and proves possession when exchanging the code for tokens, so an intercepted code is useless. The login page opens in the system browser through ASWebAuthenticationSession or Android Custom Tabs, never an embedded WebView, so the app cannot see the password and the user gets shared sessions and password managers. I check the state parameter and use verified https redirects where possible. Libraries like AppAuth handle these details. Tokens are stored in the Keychain or Keystore-backed storage, access tokens are short-lived, and refresh happens in one place with a lock to avoid races. Logout revokes tokens on the server and clears local data.

9. How would you migrate a large UIKit or Android View app to SwiftUI or Jetpack Compose without stopping feature work?

A strong answer covers:

  • Migrate incrementally using interop (ComposeView, UIHostingController and the reverse)
  • Build the design system components first
  • New screens in the new framework, old screens migrated by priority
  • Separate state and logic into ViewModels so the UI layer can be swapped
  • Measure performance and app size, and agree on standards and training
Show model answer

I would never do a big-bang rewrite. Both frameworks have good interop: ComposeView and AndroidView on Android, UIHostingController and UIViewRepresentable on iOS, so old and new UI can live side by side. First I build the design system in the new framework, like buttons, text styles and list rows, because every screen needs them. Then the rule is that new screens use the new framework, and existing screens are migrated when we touch them for features or when they cause many bugs. Before converting a screen, I make sure its logic lives in a ViewModel that exposes state, so only the view layer changes. I add screenshot tests to catch visual differences and measure scroll performance, startup and app size, since adding a framework adds weight. Team guidelines, code review and shared examples keep the code consistent during the long transition.

10. Millions of users have data in your app local database. How do you change the schema safely?

A strong answer covers:

  • Write explicit, versioned migrations for every schema version
  • Test migrations from every old version with real-like data
  • Avoid destructive fallback unless the data can be fully re-downloaded
  • Handle large tables carefully: transactions, batching, startup time
  • Plan for failure: backups, telemetry and staged rollout
Show model answer

Users can jump from any old version to the newest, so I keep an explicit migration for every version step and test the full chain, for example 3 to 7, not only the last step. Room MigrationTestHelper, Core Data or SwiftData lightweight migrations and SQL scripts for SQLite are verified in automated tests against databases created by real old builds with realistic data. I avoid destructive migrations unless the data is a pure cache that can be fully re-downloaded, because wiping offline edits loses user work. Each migration runs in a transaction so a failure leaves the old data intact. For big tables I watch the time it adds to startup and may migrate lazily or in the background. I log migration failures to telemetry, and I ship schema changes through a staged rollout so a bad migration reaches only a small group before we can halt it.