Building one codebase for both iOS and Android, through frameworks like React Native or Flutter, promises a single build that runs the same way everywhere. What it actually delivers is a single build that runs mostly the same way everywhere, and the gap between those two things is where most cross-platform bugs live.
The parts of an app most likely to diverge between platforms aren’t the parts developers spend the most time on. They’re the ambient system behaviour underneath the interface: how permissions prompts appear and get dismissed, how system dialogs interrupt a flow, how deep links get handled when the app isn’t already open, how push notifications behave in the background, how a keyboard resizes a screen. None of that is core application logic, and none of it is fully controlled by the framework, because it’s governed by the operating system underneath, and iOS and Android don’t agree on how any of it should work.
This is a known and repeatedly documented pattern across teams building in both frameworks. Sharing one codebase reduces how much logic has to be written twice, but it doesn’t remove the underlying platform differences, it just moves where those differences show up. A button might render identically on both platforms and still respond to a system-level interruption, an incoming call, a low battery warning, a permissions prompt, in two different ways, because the interruption isn’t coming from the shared code at all.
Flutter and React Native handle this trade-off differently. Flutter draws its own UI from scratch using its own rendering engine, which gets closer to pixel-identical output across platforms, at the cost of having to explicitly rebuild platform-native behaviour like accessibility features and system font handling that other frameworks inherit for free. React Native leans on native UI components directly, which keeps more of that platform-native feel but reopens the door to the small visual and behavioural inconsistencies a shared codebase was supposed to remove in the first place. Neither approach eliminates the divergence, they just move where it shows up.
What actually breaks first in a cross-platform build is rarely the feature everyone tested carefully. It’s the interaction nobody thought to test on both platforms specifically, because on paper it’s the same line of code running in both places. Push notification behaviour after the app has been backgrounded for a while is a common one. So is what happens when a user grants a permission on one platform and denies it on the other, and the app has to handle both outcomes gracefully rather than assuming the happy path.
One codebase is still worth building, for the vast majority of apps it’s the right default over maintaining two entirely separate native builds. It just isn’t a guarantee. It’s a starting point that still needs the platform-specific edges tested deliberately, not assumed. One codebase promises consistency. It never promised identical behaviour.
One codebase promises consistency. It never promised identical behaviour.
