We’re live on Product Hunt — click here to upvote

How Our AI Handles Platform-Specific Code (iOS vs Android)

(155 chars):

RI

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 StyleSheet for 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:

ConcernWhat most tutorials writeWhat our AI generatesWhere the branch actually lives
Safe area (notch, punch-hole, status bar)Manual Platform.OS === 'ios' padding<SafeAreaView edges={['top']}> from react-native-safe-area-contextInside the library — reads OS insets natively on each platform
Status barManual color + style branches<StatusBar style="dark" /> from expo-status-barExpo runtime
Navigation stacks / transitionsManual per-platform stack configExpo Router with app/_layout.tsxExpo Router compiles to native stack on both
Splash screenManual iOS launch screen + Android splashexpo-splash-screen plugin declared in app.jsonExpo build pipeline
IconsTwo icon libraries or manual glyphslucide-react-nativeOne package, RN vector output
FontsManual per-platform font loadingexpo-font useFonts()Expo runtime
Deep linkingManual URL parsing per platformexpo-linking.createURL()Expo runtime registers scheme in both Info.plist and AndroidManifest.xml
StylingPer-platform StyleSheet picksNativeWind Tailwind classesCompiled 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:

  1. 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.
  2. 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.
  3. APK build (Android) — Triggered from the editor via a generate_apk action (costs 10 credits). Produces an installable .apk you can sideload on any Android device or share with testers. This is the first time you see the Android build as it will ship.
  4. EAS Build → App Store / Play Store — For users who export their code and want to submit to the stores. The generated app.json is already valid for eas 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.json with both ios and android blocks pre-filled
  • package.json with pinned versions of expo, react-native, expo-router, nativewind, react-native-safe-area-context, lucide-react-native
  • All screens under app/ using file-based routing that compiles to native Stack.Navigator on both platforms
  • Any Platform.OS branches the AI wrote, visible and editable in the source
  • A working eas.json shape so eas build --platform ios and eas build --platform android both 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.

Start now

Ready to build your app?

Turn your idea into a production-ready React Native app in minutes.

Free tools to get you started

Questions

Frequently 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.