NK

Search

Search pages, posts, and components

All posts
Mobile9 min read

Flutter Is Not Just a UI Framework

Its own rendering engine, its own layout model, a compiled language, and a plugin boundary - Flutter replaces more of the platform than most people realise.

flutterdartarchitecturefundamentals

Flutter gets filed next to React Native as "the cross-platform UI thing", and that framing quietly misses what it is. React Native renders through the platform's own view system - a React <Text> becomes a UITextView on iOS. Flutter does not. It ships its own rendering engine and paints every pixel itself.

That single decision cascades into everything else: why Flutter apps look identical across platforms, why the binary is larger, why animations are smooth on old Android devices, why a text field sometimes feels subtly wrong, and why Flutter runs on embedded displays and desktop at all.

Calling it a UI framework is like calling a browser a document viewer. Accurate, and it understates the situation.

It brings its own renderer

The engine is C++ and it owns the surface.

Nothing on screen is a platform widget

When you write ElevatedButton, no UIButton is created. Flutter asks the OS for a canvas, then draws the button - shape, shadow, ripple, and text - with its own graphics pipeline. Impeller today, Skia historically, but the principle is unchanged.

The consequences are worth stating in both directions.

What it buys: pixel-identical output across iOS, Android, web, and desktop. No "it looks fine on my device" caused by an OEM's theming. Custom design systems are as cheap as stock ones, because everything is custom already. And you get sixty or a hundred and twenty frames per second on a device whose native toolkit would be struggling, because the pipeline is yours.

What it costs: you inherit responsibility for things platforms give away. Accessibility must be bridged explicitly through the semantics tree. Text selection, magnification, and IME behaviour are reimplementations, and they are the places where Flutter most often feels almost right. Every widget must be re-authored when a platform changes its look.

Layout is Flutter's, not the platform's

Flutter does not use Auto Layout, or the CSS box model, or Android's measure and layout. It has one protocol: constraints go down, sizes go up, parents position children - resolved in a single pass.

That is why Flutter layout errors read unlike any other framework's, and why knowledge transfers oddly. Nothing you know about flexbox quite maps, because this is a different algorithm with different failure modes.

Dart is doing more work than it gets credit for

The language choice looks incidental and is not.

Two compilers, two jobs

Dart compiles JIT in development and AOT for release. That is the whole reason Flutter can offer sub-second hot reload and ship a release build with no interpreter, no bridge, and no JavaScript engine.

React Native's architecture is shaped by the presence of a JS runtime; Flutter's is shaped by its absence. There is no serialisation boundary between your logic and your UI, because they are the same compiled program.

The concurrency model is deliberate

Dart is single-threaded per isolate, with no shared mutable memory between isolates. That eliminates data races by construction - you cannot corrupt state across threads because you cannot reach it.

The cost is that true parallelism requires message passing, and moving data between isolates means copying it. That is a real constraint, and the reason "just put it on a background thread" is not the one-line fix Flutter developers coming from Kotlin or Swift expect.

Accessibility has to be rebuilt, not inherited

This is the clearest illustration of what owning the renderer costs.

On a native toolkit, a button is accessible because it is a platform button - VoiceOver and TalkBack already know what a UIButton is. Flutter draws a rectangle. Nothing about those pixels tells a screen reader anything.

So Flutter maintains a parallel semantics tree, built alongside the render tree and pushed to the platform's accessibility APIs:

Semantics(
  button: true,
  label: 'Add to cart',
  onTap: _addToCart,
  child: MyCustomButton(),
)

Most stock widgets populate this for you, hence accessibility works without effort until the moment you build something custom. Draw a control with CustomPaint and a GestureDetector and it is, to a screen reader, an undifferentiated region - unless you say otherwise.

The upside is that semantics are explicit and inspectable: the semantics debugger shows exactly what assistive tech will see, which is more than most web stacks offer. The cost is that it is your job, and it is invisible until someone reports it.

The platform boundary is explicit

Flutter replaces the UI layer, not the operating system. Everything the OS owns - Bluetooth, camera, keychain, notifications - reaches you through a deliberate boundary.

Three ways across

Platform channels pass messages between Dart and Kotlin/Swift. Asynchronous, serialised, and the standard route for most plugins. Fine for a permission request; unsuitable for streaming audio frames.

FFI calls C directly from Dart with no serialisation. This is how you use an existing native library - SQLite, an ML runtime, a codec - at close to native speed.

Platform views embed an actual native view inside the Flutter tree, for things you cannot reimplement: a map, a webview, a camera preview. They are the most expensive option, because now two rendering systems must be composited together, and they are the usual answer when someone reports jank around a map.

Knowing which of the three a package uses tells you most of what you need to predict about its performance.

One codebase, six targets

The engine is portable, so Flutter runs where it can get a surface: iOS, Android, web, macOS, Windows, Linux, and embedded devices - car dashboards, appliances, point-of-sale terminals.

That is not a marketing bullet, it is the direct consequence of not depending on platform widgets. There is no per-platform view system to port to, only an engine to build.

The toolchain is part of the deal

The last piece people underestimate is how much of Flutter is tooling rather than framework.

Three build modes, three different programs

Debug is JIT-compiled, carries the observatory and hot reload, ships assertions, and is unoptimised. It is often several times slower than release - and that is why performance conclusions drawn from a debug build are worthless.

Profile is AOT-compiled like release but keeps the timeline and tracing hooks. This is the only correct place to measure.

Release is AOT, tree-shaken, with assertions and debug info stripped.

Three artefacts from one codebase, with actually different runtime characteristics. A jank report that does not say which mode it came from is not yet a bug report.

The engine ships with your app

Every Flutter binary contains the engine: the renderer, the Dart runtime, the text stack. That is the floor on binary size, and it is the direct cost of the independence described above.

It also means your app is insulated from OS updates in a way native apps are not. A platform redesign will not restyle your buttons overnight - and a platform bug fix will not reach you either, until you rebuild against a newer engine. Whether that is a feature depends on which side of a regression you find yourself on.

Key takeaways

  • Flutter paints its own pixels. No platform widgets, and that is why output is identical everywhere - and why accessibility and text editing are Flutter's problem, not the OS's.
  • The layout algorithm is Flutter's own. Single-pass constraints, unlike flexbox or Auto Layout, with its own failure modes.
  • Dart's JIT/AOT split is what enables hot reload and a bridge-free release build. Language and architecture are coupled here.
  • Isolates trade shared memory for safety. No data races, but real parallelism means copying data.
  • Three ways across the platform boundary - channels, FFI, platform views - with very different costs. Platform views are the expensive one.
  • Portability follows from the renderer. Owning the pipeline is why embedded and desktop targets are even possible.
  • Accessibility is a parallel semantics tree you maintain, not something inherited from platform widgets.
  • Three build modes are three different programs. Only profile mode is meaningful for performance work.

FAQ

So is Flutter a game engine?

Architecturally it is closer to one than to a typical UI toolkit - its own render loop, its own scene graph, its own compositor. It is optimised for UI rather than 3D, but the shape of the thing is similar.

How does Flutter handle text if it does not use platform text views?

It ships its own text stack - shaping, line breaking, bidi, font fallback - on top of HarfBuzz and ICU. That is a substantial part of the engine, and it is why Flutter renders the same paragraph identically everywhere while a browser might not. It is also why obscure script or IME bugs surface in Flutter that never appear in a native app.

Does not using native widgets mean the app feels wrong?

Sometimes, in specific places: text selection handles, scroll physics at the edges, and IME behaviour with some keyboards. Cupertino widgets close most of the visual gap; the interaction details are where a careful user notices.

Is the larger binary size avoidable?

Not entirely - you are shipping an engine. Tree shaking, split debug info, and per-ABI builds reduce it substantially, but a Flutter app will always start from a higher floor than a native one.

Where does Flutter web fit?

It compiles Dart to JavaScript or WebAssembly and renders to canvas or the DOM depending on the renderer. It is excellent for app-like experiences and a poor fit for content sites, where you want real DOM for SEO and text selection.

Does this make Flutter better than React Native?

Different, with a clear trade. Flutter owns rendering, so it gets consistency and control and inherits responsibility for platform behaviour. React Native uses platform views, so it gets native feel for free and inherits platform inconsistency. Which is better depends on whether your design wants to look like the platform or like your brand.

Conclusion

Flutter is a rendering engine, a layout system, a compiler toolchain, and a platform-interop layer that happens to expose a pleasant widget API. Treating it as only the last of those is why people are surprised by binary size, by accessibility work, by platform view jank, and by the isolate model.

Understanding what it actually replaces makes those surprises predictable - and makes the framework's stranger decisions read as consequences rather than quirks.

Read more

For what the rendering half is doing on every frame, see How Flutter Rendering Actually Works. For the concurrency model in practice, see Flutter Isolates Explained Through a Real Example.