---
title: "33 — Mobile Apps — One Codebase, Three Ways to Reach the Pixels"
uid: mobile-apps
tags: ["flutter", "cross-platform", "react-native", "ionic", "mobile", "roadmap:frontend"]
excerpt: "No Swift, no Kotlin: every cross-platform framework reaches both platforms from one codebase. They differ by HOW code becomes pixels and HOW MUCH native UI you get."
date: 2026-08-12T18:35:05+0000
source: https://www.aveshina.my.id/en/blog/mobile-apps
---

"Learn Swift and Kotlin and write everything twice" was my mobile-app plan, and it was wrong in one important way. The model that finally clicked is small enough to hold in one line: **every cross-platform framework reaches iOS and Android from one codebase; they differ by HOW they turn that code into on-screen pixels, and HOW MUCH of the platform's native UI you actually get.** Once I separated "the language I write" from "the rendering strategy underneath," the three big names stopped being a blur and became a clean trade-off.

Here is the whole picture — one source, three rendering strategies, one phone — before I walk through each path:

```figure
<svg viewBox="0 0 760 320" xmlns="http://www.w3.org/2000/svg" class="my-6 w-full max-w-2xl" role="img" aria-label="One codebase reaching one phone through three different rendering strategies. Left: a single source-code box. Middle: three paths. Top path React Native bridges JS to REAL native components drawn by iOS and Android, so the result looks authentic on each platform. Middle path Flutter draws its OWN widgets via a Dart rendering engine, so every pixel is identical across platforms. Bottom path Ionic runs HTML, CSS and JS inside a webview wrapped with native bridges, so it is a web app shipped as a mobile app. Right: one phone where all three paths land.">
  <defs>
    <marker id="marrow" viewBox="0 0 10 10" refX="9" refY="5" markerWidth="7" markerHeight="7" orient="auto">
      <path d="M0,0 L10,5 L0,10 z" fill="#64748b"/>
    </marker>
  </defs>

  <g font-family="ui-sans-serif, system-ui, sans-serif" text-rendering="geometricPrecision">

    <!-- source -->
    <rect x="30" y="120" width="150" height="80" rx="10" fill="#e0e7ff" stroke="#6366f1" stroke-width="1.5"/>
    <text x="105" y="150" font-size="13" font-weight="700" fill="#1e1b4b" text-anchor="middle">One codebase</text>
    <text x="105" y="170" font-size="11" fill="#475569" text-anchor="middle">JS/React · Dart · web tech</text>
    <text x="105" y="188" font-size="10" fill="#64748b" text-anchor="middle" font-style="italic">write once</text>

    <!-- phone -->
    <rect x="650" y="110" width="86" height="130" rx="14" fill="#f8fafc" stroke="#cbd5e1" stroke-width="1.5"/>
    <rect x="662" y="124" width="62" height="100" rx="4" fill="#0f172a"/>
    <circle cx="693" cy="234" r="4" fill="none" stroke="#cbd5e1" stroke-width="1.2"/>
    <text x="693" y="262" font-size="10" font-weight="700" fill="#1e1b4b" text-anchor="middle">one phone</text>

    <!-- path 1: React Native -->
    <rect x="250" y="40" width="320" height="60" rx="8" fill="#fef9c3" stroke="#ca8a04" stroke-width="1.5"/>
    <text x="410" y="64" font-size="12" font-weight="700" fill="#422006" text-anchor="middle">React Native — bridge to REAL native components</text>
    <text x="410" y="82" font-size="10" fill="#475569" text-anchor="middle">JS thread ⇄ bridge ⇄ native iOS / Android views</text>
    <path d="M180,140 C210,90 230,70 248,70" fill="none" stroke="#64748b" stroke-width="1.5" marker-end="url(#marrow)"/>
    <path d="M570,70 C610,70 630,110 650,150" fill="none" stroke="#64748b" stroke-width="1.5" marker-end="url(#marrow)"/>

    <!-- path 2: Flutter -->
    <rect x="250" y="130" width="320" height="60" rx="8" fill="#fce7f3" stroke="#db2777" stroke-width="1.5"/>
    <text x="410" y="154" font-size="12" font-weight="700" fill="#500724" text-anchor="middle">Flutter — draws its OWN widgets</text>
    <text x="410" y="172" font-size="10" fill="#475569" text-anchor="middle">Dart + rendering engine paints each pixel itself</text>
    <path d="M180,160 L248,160" fill="none" stroke="#64748b" stroke-width="1.5" marker-end="url(#marrow)"/>
    <path d="M570,160 L648,175" fill="none" stroke="#64748b" stroke-width="1.5" marker-end="url(#marrow)"/>

    <!-- path 3: Ionic -->
    <rect x="250" y="220" width="320" height="60" rx="8" fill="#dcfce7" stroke="#16a34a" stroke-width="1.5"/>
    <text x="410" y="244" font-size="12" font-weight="700" fill="#052e16" text-anchor="middle">Ionic — web app in a webview</text>
    <text x="410" y="262" font-size="10" fill="#475569" text-anchor="middle">HTML / CSS / JS + thin native bridges</text>
    <path d="M180,180 C210,230 230,250 248,250" fill="none" stroke="#64748b" stroke-width="1.5" marker-end="url(#marrow)"/>
    <path d="M570,250 C610,250 630,210 650,195" fill="none" stroke="#64748b" stroke-width="1.5" marker-end="url(#marrow)"/>

    <text x="410" y="305" font-size="10.5" fill="#64748b" text-anchor="middle" font-style="italic">same source, same phone — the path is what differs</text>
  </g>
</svg>
```

## Native first: what the bar is

Before the three frameworks, the thing they're all trying to avoid: writing the app **twice**, once in Swift or Objective-C for iOS, once in Kotlin or Java for Android. That's "native" development — separate codebases, separate teams, separate release trains, and a perfectly authentic UI on each platform because you're literally using each platform's own widgets [1]. Nobody argues the result feels wrong; they argue about the cost of getting there. Cross-platform exists to pay that cost once instead of twice.

The trade-off every framework makes is a bet on the same axis: **how close to "perfectly native" does the result have to feel, and what am I willing to give up to share code?** The three below sit at three different points on that axis.

## React Native: bridge to real native components

React Native is the one that most directly asks "what if a <View> just meant the platform's real UIView?" You write JavaScript and React; the framework runs your JS in its own thread and **bridges** each UI instruction over to the device, where it's rendered using the actual native widgets — UIView on iOS, android.view.View on Android [2]. A button isn't a drawing of a button; it's the platform's button.

That's the part that clicked for me. The same <Text> or <ScrollView> I write becomes a different real component on each platform, which is why a React Native app **looks and feels native out of the box** — menus swipe right, switches toggle with platform physics — without me touching either SDK (software development kit — the platform's official toolkit). The cost of that authenticity is the bridge: UI work has to cross from the JS thread to the native thread and back, which is why list scrolling or gesture-heavy animation could historically feel janky (stuttery). (The **New Architecture** — the Fabric renderer plus JSI, a faster direct channel that removes the async bridge — is the framework's answer to exactly that.)

The payoff is skill reuse. If I already think in React components, props, and hooks, React Native lets me bring that way of thinking to mobile and reach the native UI I couldn't otherwise afford to write. Native device features — camera, GPS, push — come through community modules or react-native-* libraries rather than raw SDK calls.

## Flutter: draw your own widgets

Flutter takes the opposite bet. Instead of bridging to the platform's widgets, it **ships its own rendering engine (Skia/Impeller) and draws every widget itself**, straight to the canvas [3]. You write Dart, not JavaScript; the framework paints a Material (Google's design style) or Cupertino (Apple's design style) widget on iOS exactly as it does on Android, because neither OS is being asked to render anything — Flutter is.

The part I had to straighten out: "draws its own widgets" sounds like a hack until you realize it's the source of Flutter's biggest strength, **pixel consistency**. The same app looks identical down to the sub-pixel on every platform, because the OS isn't in the rendering loop. There's no bridge to jank against; UI runs at 60–120fps because there's nothing to cross. Hot reload (seeing changes instantly without a full rebuild) is famously fast, and the widget library is rich enough that complex layouts come together quickly.

The cost is the mirror image of React Native's strength. Because Flutter isn't using the platform's widgets, it doesn't *automatically* feel native — it feels like Flutter, which is usually close but not identical to the real thing. And "write Dart" is a real ask: it's a new language, a new toolchain, and a smaller talent pool than the JS/React world. If consistency and performance across platforms matter more than hewing to each OS's native idioms, Flutter wins on the merits.

## Ionic: a web app in a webview

Ionic is the path that reuses the most of what a frontend developer already has. You write **HTML, CSS, and JavaScript** — optionally with React, Vue, or Angular on top — and Ionic wraps that web app inside a **webview** (a native container that's effectively a sandboxed browser) and exposes device features through bridges like Capacitor [4]. The app you ship is, at its core, a web app that happens to install from an app store and can reach the camera.

This is the path with the lowest barrier and the highest code reuse if you already have a responsive web app: a lot of the same code can run on the web, on iOS, and on Android. Ionic's component library is styled to mimic native UI, so a button *looks* like an iOS button on iOS and an Android button on Android — but it's still a web element in a webview, not the real native control.

The trade-off is the one web developers already know is coming. A webview is a browser, with a browser's startup cost, memory footprint, and rendering characteristics. For content-driven apps — forms, lists, dashboards — the gap is small and Ionic is genuinely a fast way to ship to three platforms. For anything animation-heavy or latency-sensitive, the webview shows through, and React Native or Flutter will feel noticeably better.

## How they line up

Same axis, three points:

- **React Native** — JS/React; bridges to **real native components**; authentic look/feel; shared React skills; the bridge is the historical bottleneck.
- **Flutter** — Dart; **draws its own widgets**; pixel-identical across platforms; excellent performance; new language and toolchain to learn.
- **Ionic** — web tech (HTML/CSS/JS); runs in a **webview** with native bridges; maximum reuse of existing web skills and code; closest to "just ship the web app as a mobile app," with a webview's performance ceiling.

The question that picks between them isn't "which is best" — it's "what am I already fluent in, and how native does the result have to feel." Coming from React and wanting authentic platform UI: React Native. Wanting one high-performance, perfectly consistent UI everywhere and willing to learn Dart: Flutter. Already sitting on a web app and needing it in an app store by next week: Ionic.

## How I use this

The habit these notes left me with is refusing to compare the three on a single axis. The real decision is two questions asked together — *what language do I want to write*, and *who draws the pixels*. Answer those two and the framework usually picks itself: React skills plus native components points at React Native; a new language plus a self-rendering engine points at Flutter; existing web code plus a webview points at Ionic. There's no wrong answer, just three different bets on the same trade-off.

## References

[1] roadmap.sh, "Mobile Apps," 2024. [Online]. Available: [https://roadmap.sh/frontend/mobile-apps](https://roadmap.sh/frontend/mobile-apps)

[2] Meta Open Source, "React Native — Learn the Basics," React Native Docs, 2024. [Online]. Available: [https://reactnative.dev/docs/getting-started](https://reactnative.dev/docs/getting-started)

[3] Google, "Flutter — Build apps for any screen," Flutter Documentation, 2024. [Online]. Available: [https://docs.flutter.dev](https://docs.flutter.dev)

[4] Ionic, "Ionic Documentation — Introduction," 2024. [Online]. Available: [https://ionicframework.com/docs/intro](https://ionicframework.com/docs/intro)

```quiz
Q: What does React Native do with your JavaScript UI code?
- It compiles it directly into native bytecode
- It bridges each UI instruction over to be rendered with the platform's REAL native widgets
- It draws its own widgets on a canvas, ignoring the OS
- It runs the code inside a webview
correct: 1
explain: React Native runs your JS in its own thread and bridges UI calls to the actual native components — a real UIView on iOS, a real android.view.View on Android — which is why it feels native out of the box.

Q: Which framework ships its OWN rendering engine and draws every widget itself?
- React Native
- Flutter
- Ionic
correct: 1
explain: Flutter's engine (Skia/Impeller) paints each widget directly, so the same UI looks pixel-identical across platforms — at the cost of not using the OS's real native controls.

Q: Ionic apps are, at their core, a ______ wrapped in a native container.
- compiled binary
- web app running in a webview, with native bridges for device features
- Dart program
correct: 1
explain: Ionic wraps HTML/CSS/JS inside a webview (a sandboxed browser) and reaches device features through bridges like Capacitor — maximum web-skill reuse, with a webview's performance characteristics.

Q: The single trade-off axis all three frameworks sit on is…
- bundle size
- how close the result feels to "perfectly native" vs how much code you can share
- number of supported platforms
correct: 1
explain: Every cross-platform framework bets on the same axis — native fidelity versus code reuse. Each picks a different point: React Native (native UI), Flutter (consistent self-drawn UI), Ionic (maximum web reuse).

Q: I already have a responsive web app and need it in the app store quickly, reusing as much code as possible. The natural pick is…
- Flutter
- React Native
- Ionic
correct: 2
explain: Ionic wraps existing web tech (HTML/CSS/JS) in a webview, so an existing web app reaches iOS and Android with the least rewrite. Flutter and React Native would both mean a larger rewrite in Dart or React-native components.
```
