Cross-platform candidates get asked the same five things in both loops: screen lifecycle, memory leak diagnosis, serial against concurrent execution, image loading in a scrolling feed, and MVVM against MVC. The framing survives the platform swap. The mechanism underneath does not, because ARC is not a tracing collector, a dispatch queue is not a coroutine dispatcher, and a UIViewController is not destroyed on rotation while an Activity is. Below, each theme is split into the half of your answer that carries over and the half you have to replace before Thursday.
The five themes and what actually swaps
Keep the structure of every answer and rewrite one clause. That clause is different each time, and knowing which one it is stops you saying "the ViewModel survives rotation" in an iOS interview, which is the most common tell that a candidate learned the concept on the other platform.
| Theme | The half that carries over | The half you swap |
|---|---|---|
| Lifecycle | Set up cheaply on create, start work when visible, release it when not. | Android recreates the screen on configuration change and can kill the process. iOS does neither. |
| Memory leaks | Find what still holds a strong reference to a screen that should be gone. | Retain cycles on iOS, long-lived GC roots on Android. Memory Graph against LeakCanary. |
| Concurrency | UI on the main thread, never block it, know your exclusivity primitive. | GCD queues and actors against dispatchers, suspension, and structured cancellation. |
| Feed images | Three tiers of cache, downsample at decode, cancel on reuse, decode off the main thread. | You hand-roll cancellation on iOS. Glide or Coil does it for you, so the probing moves to bitmap memory. |
| Architecture | Keep UI framework types out of the layer you unit test. | ViewModel is a framework class with a survival guarantee on Android and a naming convention on iOS. |
1. View controller lifecycle against Activity and Fragment lifecycle
Shared core: describe the callbacks as a contract about visibility, then say what you attach and release at each edge. Location updates, timers, sensor listeners and video playback belong to the visible window, not the created window.
| Probe | iOS | Android |
|---|---|---|
| Callback order | loadView, viewDidLoad, viewWillAppear, layout passes, viewDidAppear, then viewWillDisappear and viewDidDisappear. | onCreate, onStart, onResume, then onPause, onStop, onDestroy, with onRestart before a second onStart. |
| Runs once against runs many times | viewDidLoad runs once per view load, appearance callbacks run every time. | onCreate runs once per instance, but a rotation makes a new instance, so once is not once per screen. |
| Rotation | No recreation. You get viewWillTransition(to:with:) and a layout pass on the same object. | Default behaviour destroys and recreates the Activity. Half of Android lifecycle lore exists to handle this. |
| Saving state | Instance properties are simply still there. | onSaveInstanceState into a Bundle, or a SavedStateHandle. Small values only. |
| Second lifecycle | None. A view controller owns one view. | A Fragment has an instance lifecycle and a view lifecycle. This is the real question. |
| Teardown signal | deinit, which fires when the last strong reference goes. | onDestroy, which is not a destructor and is not guaranteed if the process is killed. |
The Fragment split is where senior candidates separate from mid. When a Fragment is replaced and added to the back stack, its view is destroyed at onDestroyView while the Fragment object stays alive. Registering an observer with this instead of viewLifecycleOwner means the old observer is never removed, so when the user navigates back and a new view is created you now have two observers writing to a view that only one of them still owns. Say that out loud and the follow-ups get friendlier.
The rotation trace, callback by callback
This is the checkable one. Interviewers ask you to narrate a device rotation while a network call is in flight, and the two traces do not resemble each other.
Android, default configuration
onPause, onStop, then onSaveInstanceState on API 28 and above, where older releases ran it before onStop instead, then onDestroy with isChangingConfigurations true, then a new instance running onCreate with the saved Bundle, onStart, onRestoreInstanceState, onResume. The Jetpack ViewModel is not recreated, which is precisely why the in-flight call belongs in viewModelScope and not in the Activity.
iOS, same rotation
One object throughout. viewWillTransition(to:with:) fires with a coordinator, then viewWillLayoutSubviews and viewDidLayoutSubviews run against the new size. No appearance callbacks, no state saving, no recreation. The in-flight call is untouched because nothing was torn down. Candidates coached on Android often invent a restoration step here, and the invention is audible.
The Android follow-up that has no iOS twin
"Now the user backgrounds the app for twenty minutes and the system reclaims the process." Everything in memory is gone, including the ViewModel. Only what you wrote into saved state comes back, and the Bundle travels through a Binder transaction with roughly a one megabyte budget shared across the process, so putting a bitmap or a list of a thousand items in there earns a TransactionTooLargeException. Store an identifier, re-fetch the rest.
2. Memory leak diagnosis
Shared core: name the tool, describe the reference path, then name the fix. Both platforms are really asking whether you have ever read a retention path rather than guessed at one.
| Probe | iOS | Android |
|---|---|---|
| Why a leak happens | ARC counts references and has no cycle collector, so two objects holding each other are immortal. | A tracing collector reclaims cycles fine. A leak needs a path from a GC root: a static field, a singleton, a live thread, a pending message. |
| The textbook example | A closure capturing self strongly, a delegate declared strong, a target-based Timer never invalidated. | An inner class or a delayed Runnable holding the implicit outer Activity, a singleton handed an Activity context, a listener never unregistered. |
| The fix vocabulary | [weak self] in the capture list, weak var delegate, unowned only when the lifetime is genuinely guaranteed. | Application context for anything long-lived, viewLifecycleOwner for observers, cancel in onDestroyView, scope coroutines to a lifecycle. |
| Tooling answer | Xcode Memory Graph Debugger for the cycle itself, Instruments Allocations and Leaks for growth over time. | LeakCanary watches destroyed Activities, Fragments, views and ViewModels, then reports the shortest strong path to a root. Android Studio profiler for heap dumps. |
| Trap follow-up | "When is unowned a crash?" After the referenced object is deallocated, unlike weak, which simply becomes nil. | "Does a leaked Fragment show up as a leaked Activity?" Often yes, because the Fragment holds the host, so read the whole path, not the first line. |
3. Serial and concurrent queues against coroutine dispatchers
Shared core: two rules that never change platform. Touch UI only on the main thread, and know the primitive that gives you mutual exclusion. Everything after that swaps.
| Concept | iOS | Android and Kotlin |
|---|---|---|
| Concurrent work pool | A global concurrent DispatchQueue with a QoS, or an unstructured Task. | Dispatchers.Default, parallelism tied to core count, for CPU-bound work. |
| Blocking calls | No separate pool. Blocking a global queue spawns more threads, which is how thread explosion happens. | Dispatchers.IO, elastic up to 64 threads or the core count if that is larger, precisely so blocking is survivable. |
| Serial execution | A serial queue, or an actor, or @MainActor for UI state. | limitedParallelism(1) on a dispatcher, or a Mutex, which is not reentrant. |
| Reader and writer | Concurrent queue with writes dispatched using the barrier flag. | Usually a Mutex, or an immutable snapshot in a StateFlow, rather than a barrier idiom. |
| The deadlock question | DispatchQueue.main.sync called from the main thread deadlocks. Say why: the queue is waiting for itself. | A blocking call or runBlocking on Dispatchers.Main freezes frames and can trip an ANR at the five second input dispatch limit. |
| Cancellation | GCD work items are awkward to cancel. Swift concurrency gives a task tree and cooperative cancellation checks. | Structured by default. A cancelled scope cancels its children, and viewModelScope cancels when the ViewModel clears. |
The distinction worth stating unprompted: a coroutine suspends without owning a thread, so "how many threads does this create" is the wrong mental model on the Kotlin side and exactly the right one on the GCD side. If your Swift answer is already built on async, await and actors rather than queues, the two answers converge, and saying so is better than pretending they were always the same.
4. Image loading and caching in a feed
Shared core, four clauses worth memorising: three tiers of cache, downsample at decode time, decode off the main thread, cancel the request when the cell is recycled. Then the arithmetic, which interviewers like because it is checkable. A 1080 by 1080 image decoded at four bytes per pixel occupies roughly 4.4 MB in memory, so ten visible cells cost about 44 MB no matter that the JPEG was 200 KB on the wire. Downsampled to a 300 point cell it is around 350 KB, better than a tenfold reduction, and that one sentence answers most of the round.
| Step | iOS | Android |
|---|---|---|
| Memory cache | NSCache, thread safe and evicting under memory pressure, keyed by URL. | LruCache sized in bytes, usually a fraction of the per-app heap, or the library's own equivalent. |
| Disk and network | URLCache honouring HTTP cache headers, plus your own directory if you persist decoded output. | The image library's disk cache keyed on the request, plus the OkHttp cache underneath it. |
| Downsampling | CGImageSourceCreateThumbnailAtIndex with a max pixel size, so the full image never enters memory. | inJustDecodeBounds to read dimensions, then inSampleSize, and hardware bitmaps on API 26 and above to keep pixels off the Java heap. |
| Cell reuse | Your job. Cancel in prepareForReuse and re-check the index path before assigning the decoded image. | Handled for you when Glide or Coil is used idiomatically, because the request is bound to the view and cancelled on detach or rebind. |
| Main thread hazard | UIImage decodes lazily at draw time, so the hitch shows up during scrolling rather than during the download. | Decoding a large bitmap on the main thread, or churning the collector with full-size allocations. |
| Prefetch | UITableViewDataSourcePrefetching and the collection view equivalent. | RecyclerView prefetch plus the library's own preloading helpers. |
Because Android hands you the library, the interviewer usually asks what it is doing rather than whether you could do it. Expect "what happens if the same ImageView is bound twice in quick succession" and "how would you size the memory cache". On iOS the same minute is spent on the cancellation and index path check you had to write yourself. Banks that separate prompts by platform, like the sets in the interview question library, are useful here mainly because they stop you rehearsing the wrong half.
5. MVVM against MVC
Shared core: name the boundary and give the test for it. Nothing in the layer you unit test should import UIKit, SwiftUI, or the Android framework. If you cannot instantiate it in a plain JVM or Swift unit test without a device or a shim, the boundary is decorative.
| Probe | iOS | Android |
|---|---|---|
| What MVC meant here | Apple's MVC, where the view controller owns the view and slowly absorbs everything else, hence the Massive View Controller complaint. | Never really MVC. The Activity was a god object, then MVP with a view interface, then Jetpack MVVM. |
| What ViewModel is | A convention. A plain class, often an ObservableObject publishing state. | A concrete framework class scoped to a lifecycle owner and retained across configuration change. |
| The hard rule | No UIKit imports and no view controller reference, so it stays testable. | Never hold a View, Activity or Fragment reference. That is the exact leak the class exists to prevent. |
| State transport | Combine publishers, @Published, or the newer observation support in SwiftUI. | StateFlow collected with the lifecycle, or LiveData in older codebases. |
| Declarative shift | SwiftUI makes the view a value type, so state hoisting replaces a lot of controller code. | Compose does the same, with remember and rememberSaveable drawing the configuration change line. |
| Next step up | Unidirectional flow and reducer-style composition. | MVI with a single immutable UiState plus one-shot events. |
The sentence to get right: on Android the ViewModel survives configuration change and dies with the lifecycle owner it was scoped to, while on iOS nothing needs to survive, because the view controller was never destroyed. Deliver the correct one and the architecture round is essentially over.
Where the parallel stops helping
Three places, and each is a question you cannot answer by translating from the other platform.
- Process death. A first-class Android topic with no real iOS twin. Rehearse the saved state answer on its own rather than mapping it across.
- Fragmentation and API levels. Android loops ask about minSdk decisions, behaviour changes across releases, and device variation. iOS loops ask about deployment target and availability checks, which is a much shorter conversation.
- Review and release. App Store review, entitlements and provisioning against staged rollouts, Play policy, and the ability to ship a fix quickly. Both come up in the design round and neither translates.
Where a copilot is the wrong tool for this loop
Be blunt about the fit, because mobile loops are a poor match in several specific ways.
- Windows only, which matters more here than in most roles. OfferPilot AI runs on Windows 10 and 11. There is no macOS build, no Linux build, no mobile app and no browser extension. iOS engineers overwhelmingly work from a Mac, so unless you happen to interview from a Windows machine, none of this is available to you.
- Live coding in an IDE is the wrong shape. Much of a mobile loop is building a screen in Xcode or Android Studio while the interviewer watches, or debugging a project they hand you. Reading a suggestion mid-build breaks the narration those rounds are graded on.
- Capture exclusion is narrower than people assume. The overlay is excluded from screen capture at the operating system level, so it does not appear in Zoom, Teams, Meet, OBS or screenshots, while staying visible on your own monitor. It does nothing about a webcam, a lockdown browser, a second-device camera, eye tracking, or a person watching in the room. Follow whatever rules the process you were sent actually sets.
- Device-specific follow-ups. "Open the layout inspector" or "profile this and tell me what you see" cannot be prompted usefully. Those come from hours in the tool.
What it is genuinely useful for in a two-platform week is narrower: the spoken breadth rounds where a prompt restores a callback order or a definition you already knew, and building a company and role specific question bank beforehand so you rehearse the Android half and the iOS half separately instead of blurring them together.
How to spend the week you have
Take the five themes above and write both columns out by hand, one page per theme. Then rehearse them aloud in the order the interviews fall, naming the platform at the start of each answer as a deliberate cue. Most cross-platform candidates do not fail on knowledge. They fail by answering the Android question with the iOS mechanism thirty seconds after a context switch. Naming the platform first fixes almost all of it.
Related guides
- Data Scientist Product Sense Interview Questions: 4-Move Answer
- Data Engineer SQL Interview Round: What to Expect in 45 Minutes
- DevOps Interview vs SRE Interview: Two Different Question Sets
- SDET Interview Test Case Design: Writing Cases for File Upload
- SRE Interview Linux Troubleshooting: Triage Order to Narrate