How Our AI Handles Platform-Specific Code (iOS vs Android)
(155 chars):
By Riya
25th Jul 2026
Last updated: 25th Jul 2026
The dirty secret of "cross-platform" is that it isn't. You can write one React Native codebase, but if you actually ship it, you'll spend the last 20% of the project untangling a keyboard that jumps on iOS but not Android, a status bar that eats your header on Pixel devices, a splash screen that looks right in the simulator and wrong in TestFlight, and a login flow that opens Safari on the web build and freezes the native one.
An AI mobile app builder has to solve this before the user ever sees the "export code" button — because the moment a prompt like "add a login screen" becomes a broken keyboard on iOS, trust collapses. So we spent a lot of time thinking about React Native platform specific code and how our generator should emit it.
This post is the honest architectural answer. It covers where our AI writes explicit Platform.OS branches, where it deliberately doesn't, how the app.json split between iOS and Android happens, and what the tradeoffs look like when a generated app actually lands on a device.
The default: one prompt, one codebase, two platforms
Before we get to any branching, it helps to see the default state.
When a user types "build me a habit tracker with a home feed and a settings page" into RapidNative, our AI generates a single React Native project. It doesn't emit an iOS variant and an Android variant. It emits one Expo Router project — app/, components/, app.json, package.json — that is expected to compile and run on both iOS and Android from the same source files.
That single-source expectation is a deliberate architectural choice, and it drives everything downstream:
- The system prompt describes exactly one framework: Expo + React Native + NativeWind.
- Every screen uses the same primitives:
View,Text,Pressable,ScrollView,FlatList,SafeAreaView. - Styling is a single language: NativeWind (Tailwind) classes compiled to
StyleSheetfor both platforms. - Routing is Expo Router (file-based) — one route tree, one navigator, compiled natively per platform.
- Icons are
lucide-react-native— one package, both platforms.
The result is that ~95% of the code the AI writes contains zero platform conditionals. And that's the point. If the AI reached for Platform.OS on every screen, the generated code would be unreadable and hard to export cleanly.
But 95% isn't 100%. There are places where you can't hide the difference between iOS and Android behind an abstraction — and in those places, the AI branches deliberately. Let's look at where.
The three places our AI writes explicit Platform.OS checks
If you grep the skill files our coding agent reads before generating code, you'll find three explicit Platform.OS patterns. Not thirty. Not a floor of Stack Overflow snippets. Three.
1. KeyboardAvoidingView behavior
Any screen with a text input — login, signup, settings, comment composer — needs to shift its content when the keyboard appears. React Native ships a KeyboardAvoidingView primitive that does this, but the behavior it uses is different on each platform.
On iOS the correct behavior is padding. On Android it's height. Get this wrong and one of the platforms will either overlap the keyboard or leave a weird empty band above it.
Our AI is instructed to write this exact pattern whenever it generates a keyboard-facing screen:
<KeyboardAvoidingView
behavior={Platform.OS === 'ios' ? 'padding' : 'height'}
className="flex-1"
>
{/* form content */}
</KeyboardAvoidingView>
It's a one-line branch, and it's the single most common Platform.OS check across every app the model generates. It has to be explicit because there's no cross-platform default that behaves correctly on both — the abstraction stops at the primitive.
2. Auth flows: native deep-link vs. web redirect
The second branch shows up in OAuth flows — specifically, Google login and any other provider that requires an external browser session.
On native (iOS/Android) the correct flow is expo-web-browser.openAuthSessionAsync(), which opens an in-app browser and returns control to the app via a deep link when the user finishes signing in. On the web preview build the correct flow is a full-page redirect: window.location.href = url.
So the AI generates this:
if (Platform.OS === 'web' && typeof window !== 'undefined') {
window.location.href = startData.url;
} else {
const result = await WebBrowser.openAuthSessionAsync(
startData.url,
redirectTo
);
}
This branch matters because our preview runs in the browser (more on that below), and if the AI generated a native-only flow, the preview would silently fail. Users would prompt "add Google login," click the button in preview, and nothing would happen.
3. Storage: avoiding native-only imports on web
The third branch is a static import problem, not a runtime problem. @react-native-async-storage/async-storage is the standard React Native storage layer, but importing it at the top of a file that also runs in react-native-web will crash the web build.
So when the AI generates code that persists tokens or user preferences, it either uses a cross-platform wrapper or dynamically imports AsyncStorage inside a Platform.OS !== 'web' guard. This is the least visible of the three branches, but it's the one that most often surprises developers who write React Native by hand.
That's the full list. Everything else — safe areas, status bars, navigation stacks, splash screens, adaptive icons — is handled by abstractions that already know about the platform difference.
Why we avoid Platform.OS almost everywhere else
Every explicit Platform.OS branch is technical debt. It's a maintenance point, a testing surface, and a place where a future refactor can break one platform and leave the other silently working. So the design goal for our AI is: push platform awareness down into the abstractions, and keep the generated code readable.
Here's how each common concern gets handled without a manual branch:
| Concern | What most tutorials write | What our AI generates | Where the branch actually lives |
|---|---|---|---|
| Safe area (notch, punch-hole, status bar) | Manual Platform.OS === 'ios' padding | <SafeAreaView edges={['top']}> from react-native-safe-area-context | Inside the library — reads OS insets natively on each platform |
| Status bar | Manual color + style branches | <StatusBar style="dark" /> from expo-status-bar | Expo runtime |
| Navigation stacks / transitions | Manual per-platform stack config | Expo Router with app/_layout.tsx | Expo Router compiles to native stack on both |
| Splash screen | Manual iOS launch screen + Android splash | expo-splash-screen plugin declared in app.json | Expo build pipeline |
| Icons | Two icon libraries or manual glyphs | lucide-react-native | One package, RN vector output |
| Fonts | Manual per-platform font loading | expo-font useFonts() | Expo runtime |
| Deep linking | Manual URL parsing per platform | expo-linking.createURL() | Expo runtime registers scheme in both Info.plist and AndroidManifest.xml |
| Styling | Per-platform StyleSheet picks | NativeWind Tailwind classes | Compiled to StyleSheet for both |
This table is the real answer to "how does your AI handle iOS vs Android." The AI's job isn't to branch per platform — it's to pick abstractions whose authors already did the branching correctly. Every entry in that table is a place where explicit Platform.OS code used to be common and now isn't, because the ecosystem moved.
That matches the guidance in the official React Native platform-specific code docs — reach for Platform.OS or Platform.select only when you can't push the difference into a library.
The app.json split: where iOS and Android configs diverge
Runtime code isn't the only place platforms diverge. The build config splits too, and the AI has to generate both halves correctly on the first prompt — because you can't easily fix app.json from a chat message once your build pipeline is running.
When the AI scaffolds a new project, it writes an app.json (Expo config) that always includes both an ios block and an android block. Here's the shape:
{
"expo": {
"name": "Habit Tracker",
"slug": "habit-tracker",
"version": "1.0.0",
"orientation": "portrait",
"icon": "./assets/icon.png",
"userInterfaceStyle": "automatic",
"ios": {
"supportsTablet": true,
"bundleIdentifier": "com.habittracker.app",
"infoPlist": {
"ITSAppUsesNonExemptEncryption": false
}
},
"android": {
"adaptiveIcon": {
"foregroundImage": "./assets/adaptive-icon.png",
"backgroundColor": "#ffffff"
},
"package": "com.habittracker.app"
},
"plugins": [
"expo-router",
["expo-splash-screen", {
"image": "./assets/splash.png",
"resizeMode": "contain",
"backgroundColor": "#ffffff"
}]
]
}
}
Three things worth flagging:
The bundle identifier and Android package name are generated from the app slug. They have to be unique per app or the App Store and Play Console will reject them. Our AI derives both from the project's slug so they line up automatically — no chance of the AI naming an iOS bundle com.example.app while the Android package is com.myapp.production.
ITSAppUsesNonExemptEncryption: false is included by default. This is a small but critical iOS App Store requirement — without it, every TestFlight submission asks you to answer an export compliance question. Setting it in Info.plist upfront saves you from the same annoying question every release.
The adaptive-icon.png reference is only included if the asset actually exists. The AI's skill files explicitly tell it to check before writing — a missing adaptive-icon.png reference will crash the Android build with a cryptic error, and that's exactly the kind of thing that would make an AI-generated app feel broken before you've even edited it.
Runtime permissions (NSCameraUsageDescription, Android permissions array) are a deliberate omission from the default scaffold — they get added only when the user prompts for a feature that needs them (camera, location, notifications). This keeps the App Store review process predictable: no ghost permissions asking for reasons.
The preview problem: platform code that runs on web
Here's the design constraint that makes this whole architecture interesting.
RapidNative's real-time preview doesn't run on an iOS simulator or an Android emulator. It runs in the browser, via react-native-web, using an in-browser Metro bundler. That means every time a user prompts a change and sees it render live, the code is executing with Platform.OS === 'web'.
This has two consequences that shape how the AI writes platform code.
First: the AI can never assume that a native-only API will exist at preview time. If it generates expo-camera code, that code has to degrade gracefully — showing a placeholder in the web preview and only trying to open the real camera on a device. If it generates AsyncStorage, that has to be behind a platform guard or the preview crashes.
Second: the preview can't fully test platform-specific behavior. You can see your app render, click through it, and confirm that navigation works. But you can't see the iOS-specific keyboard padding kick in, or the Android status bar color, or the real deep-link handler. Those show up only when the user scans the QR code and opens the app in Expo Go on their actual phone — or after they hit "Generate APK" and install the build on Android.
We could have solved the second problem by streaming builds to a real simulator, but the round-trip time would be 30 seconds per prompt. Web preview keeps the loop under a second, and that speed is what lets a user go from prompt to visible screen without losing flow. The tradeoff is worth it — as long as the AI is disciplined about not writing code that only works on one runtime.
This is also why the AI leans so hard on Expo abstractions in the first place. Every abstraction has a react-native-web fallback baked in, so the preview keeps working even for features that only fully activate on device.
Testing platform code: preview, QR, and native builds
The full testing loop for platform-specific behavior in a RapidNative project looks like this:
- Preview (browser,
react-native-web) — Verifies the app renders, navigation works, forms submit, and no native-only import crashed the bundle. Runs continuously as the user prompts changes. - Expo Go on a physical device — Users scan a QR code from the editor and load the app inside Expo Go. This is where iOS-specific keyboard behavior, real safe area insets, native fonts, haptics, and deep links first behave for real.
- APK build (Android) — Triggered from the editor via a
generate_apkaction (costs 10 credits). Produces an installable.apkyou can sideload on any Android device or share with testers. This is the first time you see the Android build as it will ship. - EAS Build → App Store / Play Store — For users who export their code and want to submit to the stores. The generated
app.jsonis already valid foreas build, so the transition from RapidNative preview to a real store submission doesn't require config surgery.
The important design point: the AI writes code that has to pass all four of these environments. If it emits something that only works in preview (say, a web-only URL scheme) or only works on device (say, a native-only import outside a Platform guard), the user will hit the failure at whichever step exposes it — and the failure will feel like the AI's fault, even if the underlying code looks fine.
That's why the three explicit Platform.OS branches — keyboard, auth, storage — are non-negotiable in the AI's playbook. They're the smallest set of branches that keep all four environments green.
When you export the code, what do you actually get?
Because every RapidNative project can be exported as a full Expo codebase, it's worth being explicit about what the platform-code story looks like in the exported repo, not just in the editor.
You get a standard Expo project:
app.jsonwith bothiosandandroidblocks pre-filledpackage.jsonwith pinned versions ofexpo,react-native,expo-router,nativewind,react-native-safe-area-context,lucide-react-native- All screens under
app/using file-based routing that compiles to nativeStack.Navigatoron both platforms - Any
Platform.OSbranches the AI wrote, visible and editable in the source - A working
eas.jsonshape soeas build --platform iosandeas build --platform androidboth work
The stack is intentionally the same stack a senior React Native engineer would pick if they were starting from scratch in 2026: Expo 54, React Native 0.81, Expo Router 6, NativeWind 4. Nothing exotic, nothing that ties you to RapidNative after export. This matters if you plan to hand the code to a developer later — they'll recognize every file.
If you want the deeper "why Expo" argument, we wrote it up in why we chose Expo over bare React Native for AI code generation.
FAQ
Does RapidNative use Platform.select in generated code?
Rarely. Our AI's default is Platform.OS === 'ios' ? A : B for the few places it needs a branch, because a two-arm ternary is easier for a user to read and edit than Platform.select. Platform.select is technically equivalent but adds a layer of indirection that's harder to skim.
Does the AI generate .ios.tsx / .android.tsx file variants?
No. That extension pattern splits the same component across two files, which makes the codebase harder to reason about and doubles the number of places the AI has to keep in sync. Where a platform difference matters, we prefer a single file with a small explicit branch inside it.
What about tablets and iPad-specific layouts?
supportsTablet: true is set in the iOS config by default so the app doesn't get rejected for iPad. But the AI doesn't currently generate tablet-specific layouts unless prompted — most apps built on RapidNative are consumer mobile, and tablet-optimized layouts (split view, master-detail) are a separate design conversation.
Can the AI generate code that uses react-native- native modules?
Yes, as long as they're in the pinned dependency set. The AI is instructed to only import from packages listed in package.json, which prevents it from writing code that requires a native module the project doesn't have installed. This is a critical guardrail — hallucinated dependencies would break the build immediately.
What happens if my app needs a permission I didn't prompt for?
The Info.plist and Android permissions arrays are updated when the AI generates code that requires them. Asking for "add a camera screen" will trigger both the expo-camera install and the NSCameraUsageDescription addition. You don't have to touch native config by hand.
The takeaway
The honest answer to "how does your AI handle platform-specific code" is: as little as possible, on purpose.
Every explicit Platform.OS branch is a place where the abstraction failed and the AI had to reach past it. We keep the count of those branches deliberately small — three, at the moment — because that's what keeps the generated code readable, exportable, and portable to any React Native developer's hands later. The heavy lifting happens inside Expo, react-native-safe-area-context, NativeWind, and Expo Router, all of which know the difference between iOS and Android so the generated code doesn't have to.
The result is that a prompt like "build me a booking app" produces one codebase that renders in preview, runs on iOS via Expo Go, ships as an Android APK, and submits to both stores from the same eas build command. No fork, no two-branch strategy, no per-platform maintenance.
If you want to see how this plays out in practice, try prompting your first app — you'll get 20 free credits with no credit card. Start building on RapidNative, or if you're wondering how the AI handles other cross-cutting concerns, read how RapidNative handles navigation and routing in generated apps.
Ready to build your app?
Turn your idea into a production-ready React Native app in minutes.
Free tools to get you started
Free AI PRD Generator
Generate a professional product requirements document in seconds. Describe your product idea and get a complete, structured PRD instantly.
Try it freeFree AI App Name Generator
Generate unique, brandable app name ideas with AI. Get creative name suggestions with taglines, brand colors, and monogram previews.
Try it freeFree AI App Icon Generator
Generate beautiful, professional app icons with AI. Describe your app and get multiple icon variations in different styles, ready for App Store and Google Play.
Try it freeFrequently asked questions
What is RapidNative?
RapidNative is an AI-powered mobile app builder. Describe the app you want in plain English and RapidNative generates real, production-ready React Native screens you can preview, edit, and publish to the App Store or Google Play.
Can I export the code?
Yes. RapidNative generates clean React Native and Expo code that you can export at any time. No lock-in, no proprietary format. Hand it to your developers or keep building inside RapidNative.
Is RapidNative free to use?
Yes. You can build apps on the free plan with no credit card required. Paid plans unlock unlimited AI generations, code export, and direct publishing to the App Store and Google Play.
Do I need to know how to code?
No. Most users build apps by describing what they want in plain English. Developers can drop into the code whenever they want more control, but coding is optional.
How long does it take to build an app?
Most users have a working first screen in under a minute. A full MVP usually takes a few hours instead of the weeks or months traditional development requires.