Mobile — Cross Platform Development
Platform-Specific UI Without Forking Codebases
Direct answer
React Native gives you an escalation ladder for platform differences: inline Platform.OS checks for one-liners, Platform.select for style variants, .ios.tsx/.android.tsx file extensions for genuinely different implementations behind one import, and shared TypeScript interfaces to keep those implementations honest. Used in that order, you can ship iOS and Android experiences that each feel native while keeping a single codebase — forking is almost never necessary and usually signals the abstraction was drawn at the wrong level.
The whole promise of cross-platform breaks down the day a designer asks for an iOS-style segmented control and Android-style material ripple in the same spot. Teams either flatten everything into a lowest-common-denominator UI or start forking files wholesale. There is a disciplined middle path, and it is mostly about choosing the right tool for the size of the difference.
Key facts, with sources
- In the Stack Overflow 2024 Developer Survey, Flutter was used by 9.4% of developers versus React Native's 8.4%, and among professional developers the gap nearly vanishes at 9.21% versus 9.14%. (Nomtek (citing Stack Overflow Developer Survey))
- Among developers who build cross-platform, Statista-based figures put Flutter at about 46% adoption versus 35% for React Native, with the two frameworks together dominating the cross-platform market. (Tech-Insider)
- Instagram shares 85 to 99% of its code between iOS and Android using React Native, and Shopify reports 86% unified code across its app portfolio. (CatDoes)
- According to Flutter.dev data, nearly 30% of new free iOS apps submitted to the App Store in 2025 were built with Flutter, up from roughly 10% in 2021. (Droids on Roids (citing Flutter.dev))
- The official React Native showcase lists production apps from Meta, Microsoft, Shopify, and Amazon, including desktop targets like Messenger Desktop and Microsoft apps on Windows and macOS. (React Native Showcase)
The escalation ladder: smallest tool that works
I handle platform divergence with a strict escalation order. Level one: inline Platform.OS === 'ios' checks, only for a single value — a behavior flag, a keyboard offset. Level two: Platform.select inside StyleSheet definitions when styles differ but structure does not — shadows versus elevation being the canonical case. Level three: platform file extensions (Component.ios.tsx and Component.android.tsx) when the implementations genuinely differ, hidden behind one import path. Level four: a shared interface with fully separate implementations, reserved for things like payment sheets or native pickers.
The discipline is refusing to skip levels. A component with seven inline Platform checks should have been split into platform files; two platform files that share most of their code should collapse back into Platform.select. In code audits, both failure directions show up about equally often.
Platform.select for styling differences
The most frequent legitimate divergence is styling: iOS uses layered shadow properties while Android uses elevation; iOS headers center their titles while Android left-aligns; system fonts differ. Platform.select handles all of this without touching component structure, and because it lives inside the StyleSheet, the JSX stays identical and reviewable.
I keep these selections close to the style they modify rather than hoisting them into a global 'platform styles' file — locality makes it obvious why a difference exists. One habit worth adopting: always provide the default key when a value must exist on web or future platforms, so react-native-web builds do not silently get undefined.
import { Platform, StyleSheet } from 'react-native';
export const styles = StyleSheet.create({
card: {
borderRadius: 12,
backgroundColor: '#FFFFFF',
...Platform.select({
ios: {
shadowColor: '#000',
shadowOpacity: 0.12,
shadowRadius: 8,
shadowOffset: { width: 0, height: 2 },
},
android: {
elevation: 4,
},
default: {},
}),
},
headerTitle: {
fontSize: 17,
fontWeight: Platform.select({ ios: '600', android: '500', default: '600' }),
},
});Platform file extensions: two implementations, one import
When a component's internals genuinely differ — a date picker wrapping UIDatePicker conventions on iOS and Material pickers on Android, or an action sheet versus a bottom sheet — I split into DatePickerField.ios.tsx and DatePickerField.android.tsx. Metro resolves the right file automatically, so every consumer just writes one import and never branches.
The crucial companion is a shared types file defining the props interface both implementations must satisfy. Without it, the two files drift: someone adds a prop to the iOS version under deadline, Android silently ignores it, and you get a bug report that only reproduces on one platform. With the interface, TypeScript fails the build the moment the contracts diverge — which converts a runtime platform bug into a compile error.
// DatePickerField.types.ts — the contract both platforms must satisfy
export interface DatePickerFieldProps {
label: string;
value: Date | null;
minimumDate?: Date;
onChange: (date: Date) => void;
}
// DatePickerField.ios.tsx and DatePickerField.android.tsx both do:
// import type { DatePickerFieldProps } from './DatePickerField.types';
// export function DatePickerField(props: DatePickerFieldProps) { ... }
// Consumers import one path; Metro picks the platform file:
// import { DatePickerField } from '@/components/DatePickerField';Where platform differences are worth honoring
Not every difference deserves engineering effort. The ones users actually notice, in my experience: navigation transitions and back behavior (Android's system back button and predictive back must work), date and time pickers, action sheets versus dialogs, haptics, pull-to-refresh feel, and text selection behavior. Get these wrong and the app feels like a web page in a costume.
The ones users mostly do not notice: whether your custom buttons have ripple versus opacity feedback, minor typography weight differences, and icon style purity. I spend the platform budget on the first list and standardize the second list to whatever the design system says. A useful forcing question for each proposed divergence: would a user of this platform file a complaint, or would only a designer notice in a side-by-side? Only the first category justifies a platform split.
Guardrails that keep the codebase from quietly forking
Platform splits accumulate, and without guardrails you wake up with a half-forked codebase. Three rules I enforce on teams. First, platform files may contain only presentation and platform API calls — all business logic lives in shared hooks, so DatePickerField.ios.tsx is thin enough that ignoring it in a refactor is safe. Second, every platform-split component needs both files touched in the same pull request or an explicit reviewer note explaining why not; CI can flag PRs that modify one sibling without the other. Third, count them: I keep a lint-time inventory of platform-split files, and when the count grows past a couple dozen in a mid-sized app, that is usually a smell that the design system is under-specified rather than that the platforms truly diverge that much.
Treat every platform fork as debt with interest. Cheap to take on, fine in moderation, ruinous when unmonitored.
When to hire senior help
The framework decision is a one-way door worth a short senior consultation, because switching stacks after a year of development is effectively a rewrite. A senior cross-platform engineer can also audit whether your planned feature set has hidden native-heavy corners, like payments hardware or background location, that change the cost math before you commit. If your stack includes React Native + Python + AI, a senior engineer who owns the full product beats coordinating multiple juniors.
Bottom line
Dhairya Senjaliya ships Mobile — Cross Platform Development projects worldwide — book a scoping call to discuss your specific situation.
Common pitfalls to avoid
- ✕Picking the framework by hype rather than team composition, such as a React web team adopting Flutter and forfeiting years of transferable JavaScript expertise
- ✕Budgeting for 100% code reuse and zero platform-specific time, then discovering payments, background tasks, and widgets all require per-platform native work
- ✕Recreating navigation and interaction patterns generically so iOS swipe-back gestures and Android back-button behavior both feel wrong to native users
- ✕Ignoring the maintenance tail: never budgeting for framework major-version upgrades, so the app rots on an unsupported runtime within two years
Frequently asked questions
How do you handle platform-specific UI in React Native without duplicating code?
Use an escalation ladder: inline Platform.OS checks for single values, Platform.select for style differences like iOS shadows versus Android elevation, and .ios.tsx/.android.tsx file extensions when implementations genuinely differ. Metro resolves platform files automatically behind one import, and a shared TypeScript props interface keeps both implementations from drifting apart.
What is the difference between Platform.select and platform-specific file extensions?
Platform.select branches on values inside otherwise-identical code — ideal for styles and small constants. Platform file extensions (Component.ios.tsx, Component.android.tsx) replace the entire module per platform — ideal when the internals differ substantially, like native date pickers or action sheets. Rule of thumb: several Platform checks in one component means it should probably become platform files.
Which platform differences actually matter to users in a cross-platform app?
Navigation transitions and Android back-button behavior, native date/time pickers, action sheets versus dialogs, haptics, and pull-to-refresh feel — get these wrong and the app feels non-native. Minor differences like button ripple effects or typography weights are rarely noticed. Spend platform-specific effort on the first group and standardize the rest through your design system.
Should we choose Flutter or React Native?
Adoption among professional developers is nearly tied (9.21% versus 9.14% in the Stack Overflow 2024 survey), so the deciding factor is your team: React or JavaScript experience strongly favors React Native, while a greenfield team with no web-code-sharing needs can prefer Flutter. Both run massive production apps, so neither choice is inherently risky.
How much money does cross-platform actually save versus two native apps?
Industry cost guides put savings around 30 to 40% versus parallel native builds, driven by one codebase and one team, with production apps sharing 85 to 95% of code. Real savings depend on how much platform-specific work your feature set demands, such as payments, widgets, and background processing.
When is fully native the better choice?
Heavy 3D or AR workloads, advanced camera or audio processing, platform-first experiences like watchOS apps, or a company that already employs strong separate iOS and Android teams. For typical business, marketplace, and content apps, cross-platform is now the default choice at both startups and large companies like Shopify and Microsoft.
Bottom line: Dhairya Senjaliya ships Mobile — Cross Platform Development projects worldwide. Book a scoping call at https://dhairyasenjaliya.com/#book-call.