How Flutter Works

Lesson 1 of 5

Why Flutter Exists

"All these must be built with due reference to durability, convenience, and beauty." — Vitruvius, De Architectura, Book I (c. 15 BC)

Every cross-platform UI toolkit has to answer one question: what do you do about the buttons?

A platform has buttons already. They are drawn by code Apple or Google wrote, they behave the way users expect because they are literally the same buttons every other app uses, and they are free. The obvious move is to wrap them — you write Button, the toolkit creates a UIButton on iOS and a MaterialButton on Android, and the platform does the drawing. That is what React Native does, and the previous volume spent forty chapters on the consequences.

Flutter does not do that. Flutter ships a rendering engine inside your application and draws the button itself, pixel by pixel, using nobody's widgets. It is a strange and expensive decision, and it explains nearly everything else about the framework — including things that look arbitrary until you know it.

The big question

Flutter's bet is that controlling every pixel is worth shipping a renderer to do it. That buys exact consistency and costs you the platform's own behaviour, which is not a fixed trade: it shifts every time an OS changes its conventions, because a wrapped widget follows along for free and a drawn one has to be updated by somebody. So the question is not "is the trade good?" but "who pays for the drift, and when?" — and the answer is a team that may no longer be working on the app by the time the bill arrives.

The whole framework is Dart, and you have it

Start with something you can check on your own machine rather than a claim about philosophy. Flutter's framework is not a binary you call into; it is source, in one language, sitting in the SDK you installed.

Dart — Try it Yourself
Loading editor…

Edit here, then open the pad to run it on dartpad.dev.

Five hundred thousand lines is a lot to carry, and the reason it is worth carrying is the last point. Look at what those two design-language directories actually contain.

An analogy

A theatre company that travels with its own stage. Most touring productions adapt to each venue — they take measurements, they work with the house lighting rig, and the show is subtly different in Leeds than it was in Bristol because the rooms are different. A company that brings its own modular stage, its own rig and its own floor gets something no adapter can: the show is identical everywhere, the cues land to the same timing, and a problem solved once stays solved. It also arrives with several lorries, needs a bigger door, and cannot use the beautiful proscenium arch the venue is famous for. The trade is not subtle and it is not obviously right — it depends entirely on whether exactness or belonging matters more for what you are staging. What is not in doubt is that the second company has taken on work the first one never has to think about, including the lighting.

Both design languages are imitations

This is the fact that makes the architecture concrete, and it is checkable in a way opinions are not.

Dart — Try it Yourself
Loading editor…

Edit here, then open the pad to run it on dartpad.dev.

That freedom is real and it is the first thing people notice. The second thing, usually later and more painfully, is that a reimplementation is only as current as the person maintaining it. When Apple changes how a switch animates, a wrapped switch changes with it and a drawn one changes when Flutter ships an update and you upgrade.

Notice also which library is larger. Material has 340 classes to Cupertino's 71 — nearly five times the surface — which is not a statement about the two design languages but about who builds Flutter. Material is Google's, maintained by the team that also maintains the framework, and it shows in the coverage. If your product needs an iOS-faithful interface, that ratio is the number to weigh, because the gap is where you will end up writing the widget yourself.

A little history

Flutter began inside Chrome, as an experiment asking what a browser engine could do if it dropped the parts of the web platform that make layout slow. The team stripped out the accumulated compatibility surface, kept the rendering pipeline, and reportedly got a large speedup — which reframed the project from "a faster browser" to "a UI toolkit that happens to use a browser's rendering machinery". That origin explains several things that otherwise look like odd choices. It explains why the engine is Skia, a 2D graphics library from the browser world, rather than anything platform-native. It explains why the layout model is neither UIKit's nor Android's but a third thing with its own vocabulary of constraints. And it explains the willingness to reimplement the widgets at all, because a browser engine has always drawn its own checkboxes and select menus rather than asking the OS for them. Flutter did not decide to stop using platform widgets so much as it started from a lineage that never used them.

What it costs, in megabytes

Shipping a renderer is not free, and the number is larger than most people guess. Measured from the release engine artifact in the SDK:

Dart — Try it Yourself
Loading editor…

Edit here, then open the pad to run it on dartpad.dev.

From my notebook

I went looking for the engine size expecting to write a sentence about it and instead found the number that reorganised this chapter. One hundred and forty megabytes, raw, for one architecture — and then 36.9 compressed, which is the figure that matters and is still enormous next to anything a web developer thinks of as a dependency. What stopped me was realising the number is fixed. Every other size discussion I have had in this Library has been about growth: bundle creep, a dependency that pulled in three more, an image nobody optimised. This one is a floor, paid on day one, identical for a prototype and a product. That inverts the usual advice. You do not optimise your way under it, you decide whether to accept it — and it is the rare engineering cost that is genuinely a product decision rather than a technical one, because the only lever is whether to use the framework at all. I also note, with some discomfort, that it is the kind of number that never appears in a "why Flutter" talk.

The engine and the framework are not the same thing

It is worth separating two words that get used interchangeably and refer to completely different artefacts, because almost every confused argument about Flutter's performance is really an argument about which one somebody means.

The framework is the 541,000 lines of Dart from the first section — widgets, layout, animation, the two design languages. It is the part you write against, it is the part you can read, and it is shipped as source that gets compiled along with your own code.

The engine is separate, written in C++, and is what the 36.9 MB actually buys. It does three jobs the framework cannot do for itself: it rasterises — turning the framework's description of shapes into pixels; it lays out text, which is far harder than it sounds once scripts other than Latin are involved; and it hosts the Dart runtime, including the garbage collector.

Between the engine and the operating system sits a third piece, the embedder, which is the per-platform shim that acquires a surface to draw on, receives touch and keyboard events, and drives the frame loop. It is the only part that knows what an Android Activity or a UIViewController is. When people say Flutter is "not native", the embedder is the thin place where it touches the platform at all — and it is deliberately thin.

The rasteriser is the piece that has actually changed. For most of Flutter's life it was Skia, the same 2D graphics library Chrome uses, which fits the origin story above. Flutter has since moved to Impeller, built specifically for it, and in this SDK the switch is already the default — flutter_tools declares _impellerEnabledByDefault = true in project.dart. The motivation was a specific and long-standing complaint: Skia compiled shaders lazily, so the first time an app performed a particular kind of animation it would stutter while the shader compiled, and the jank was worst on the very first run after install. Impeller precompiles its shaders, which trades a little binary size for the absence of a class of first-run stutter.

The practical consequence for reading anything written about Flutter is that its age matters more than usual. Advice about shader warm-up, about --purge-persistent-cache, about precompiling shaders on first launch, was correct and is now largely obsolete — it describes a renderer this SDK no longer uses by default. That is worth carrying into the rest of the volume: the framework layer has been fairly stable, and the engine underneath it has been replaced.

What you get for it

The return on all that machinery is worth stating precisely, because the usual phrasing — "consistency" — is vague enough to sound like marketing.

Flutter — Try it Yourself
Loading editor…

Edit here, then open the pad to run it on dartpad.dev.

Two design languages in one tree, on one platform, from one engine. Nothing in that app consulted the operating system about how a switch should look — which is the whole architecture in a screenshot, and also the reason a Flutter app can look slightly wrong in a way a wrapped one cannot.

Where the pixels come from

One more layer deserves attention, because it is the one people find surprising and it follows directly from the decision.

Dart — Try it Yourself
Loading editor…

Edit here, then open the pad to run it on dartpad.dev.

The consequence nobody mentions

Scroll physics is the enjoyable example. There is a less enjoyable one sitting in the same layer list, and it is the strongest argument for taking this architecture seriously rather than treating it as a curiosity.

A screen reader does not look at pixels. It reads an accessibility tree — a structured description of what is on screen, what each thing is, what it is called, and what can be done to it. On a platform-widget toolkit that tree comes free, because a real UIButton already knows it is a button, already exposes its label, and already tells VoiceOver how to describe itself. Wrapping a platform widget wraps its accessibility along with everything else.

Flutter draws a rounded rectangle with some text in it. To the operating system, that is a picture. It has no idea a button is there, because in the platform's terms one is not.

This is why semantics is a top-level layer rather than a helper tucked inside widgets. Flutter has to build a parallel tree describing what its pixels mean, keep that tree in sync as the interface changes, and hand it across to each platform's accessibility APIs in the form each one expects. That machinery is substantial, it is entirely Flutter's own, and it exists solely because the drawing decision destroyed the free version.

It mostly works, and the framework's own widgets are annotated carefully. But the failure mode is worth understanding because it is quiet and it is yours: a custom widget you build out of GestureDetector and CustomPaint — the natural way to make something Flutter does not provide — is, by default, exactly the picture described above. It is tappable and invisible to a screen reader. Nothing errors, nothing warns, and the app is unusable for a group of people who will not be in your testing.

The general shape has now appeared three times in this chapter and it is the thing to carry forward. Deciding to draw everything means inheriting appearance, then behaviour, then meaning — each one a category people do not initially count as part of "drawing", and each one now your responsibility.

In the real world

The place this architecture is most often misjudged is the decision to add Flutter to an existing native app. It can be done — Flutter supports being embedded as a module — and it is frequently a worse deal than it looks, because all the costs in this chapter are front-loaded and none of the benefits are. You pay the engine's size on the first screen. You pay the drawn-widget mismatch immediately, because your one Flutter screen sits directly beside real platform screens and a user can compare them in a single scroll. And you pay a boundary at every point where the Flutter screen needs something the host app already has — the session, the navigation stack, the analytics context. Flutter's value compounds with how much of the app it owns, and an embedded module is the configuration where it owns the least. The same reasoning, run forwards, is why greenfield products are where Flutter looks strongest: nothing to sit beside, nothing to match, and the engine amortised across everything.

Common Mistakes

  1. Assuming Flutter wraps platform widgets. It draws its own; Cupertino calls no UIKit.
  2. Thinking Cupertino is "the native iOS look". It is Flutter's imitation, and its own docs say mimics.
  3. Expecting OS changes to arrive for free. A drawn widget updates when Flutter ships and you upgrade.
  4. Judging app size by growth rather than floor. The engine cost is fixed and paid on day one.
  5. Using Flutter for something tiny. The floor is the entire app when the app is small.
  6. Bolting one Flutter screen onto a native app. Costs are front-loaded; benefits compound with ownership.
  7. Forgetting that behaviour is drawn too. Scroll physics became Flutter's problem along with pixels.
  8. Treating Material and Cupertino as platform-locked. Either can be imported on either platform.
  9. Reading flutter.jar's raw size as your APK. Release builds strip, tree-shake and split per ABI.
  10. Carrying React Native's model across. There is no UIView underneath; there is a canvas.

Interview Corner

Interview Corner — the same idea at three levels. Answer yours; recognise the one above it.
Warm-up
How is Flutter different from React Native?
Model answer — React Native wraps real platform widgets — you write a component and it creates an actual UIView or Android View, so the OS does the drawing. Flutter ships a rendering engine inside the app and draws everything itself, so there is no platform widget underneath at all. That is why Flutter has both a Material and a Cupertino widget library: neither is the platform's own, they are two design languages Flutter reimplemented, and you can use either on either platform. The trade is that Flutter gets exact control and exact consistency, and gives up inheriting platform behaviour for free — including things like scroll physics, which is why the framework ships its own physics simulations.
Intermediate
What are the real costs of Flutter's approach?
Model answer — Three, and they are different in kind. First, size: you ship the engine, which is tens of megabytes compressed per architecture, and it is a floor rather than growth — the same cost for a prototype and a product, so it is a bad deal for anything small and increasingly irrelevant for anything substantial. Second, drift: a wrapped widget follows the OS when Apple or Google change a convention, and a drawn one changes only when Flutter ships an update and you upgrade, so someone has to keep paying that bill. Third, and most underrated, behaviour: drawing your own widgets means owning things nobody thinks of as appearance — the deceleration curve of a fling, whether overscroll rubber-bands or glows. Flutter handles those, but they are its problem now, and a mismatch there is the kind of thing users feel without being able to name.
Advanced
When would you argue against Flutter?
Model answer — When the app is small, when it must be small, or when Flutter would own only a little of it. The engine's cost is fixed and front-loaded, so a utility with two screens pays the full price for almost nothing, and a market on slow connections and cheap devices may rule it out on download size alone. The strongest argument against, though, is the embedded case — adding one Flutter screen to an existing native app. Every cost lands immediately: the engine on the first screen, the drawn-versus-real mismatch sitting directly beside genuine platform screens where a user can compare them in one scroll, and a boundary at every point the Flutter screen needs the host's session, navigation or analytics. Flutter's value compounds with how much of the app it owns, and that configuration is where it owns the least. I would also ask who maintains the app in three years, because the drift cost is paid by whoever is there when the platform conventions move, and that is rarely the team that chose the framework.
Company lens — how the same topic gets asked
micro1Practical: name the architectural difference from React Native and one consequence.
GoogleDeepest here — the rendering model, and why the framework owns physics and semantics.
MetaThe comparison itself: where wrapping platform widgets wins and where it loses.
AmazonThe trade as a decision: size floor, drift cost, and when you would argue against it.

Hands-on

Hands-on

Reason about the trade with numbers. Implement engineShare, computing what fraction of a build is the fixed engine; amortises, deciding whether an app is large enough to justify the floor; drawnOrWrapped, classifying what a toolkit owns; ownsBehaviour, listing what "drawing it yourself" drags along; and fitVerdict, putting them together into a recommendation. The checks below run either way — a stub scores zero, so a passing run means the behaviour is real.

Dart — Try it Yourself
Loading editor…

Edit here, then open the pad to run it on dartpad.dev.

Summary

  • Flutter draws every pixel itself rather than wrapping platform widgets, and that one decision explains the rest.
  • The framework is 662 Dart files and ~541,000 lines, all readable in the SDK you installed.
  • Thirteen layers, and physics is one of them — a toolkit that wrapped scroll views would not need it.
  • Material and Cupertino are both reimplementations, sitting as peers; neither is the platform's own.
  • Cupertino calls no UIKit. The only three mentions are doc comments, one of which says mimics.
  • Either design language works on either platform, because both are just Flutter widget libraries.
  • The engine is a size floor, 36.9 MB compressed per architecture, paid identically by a prototype and a product.
  • The floor amortises, so it is a bad deal for something small and largely irrelevant for something substantial.
  • Drawing your widgets means owning their behaviour, including deceleration curves and overscroll.
  • Embedding one Flutter screen is the worst configuration: all costs front-loaded, benefits unearned.

Next: Dart, and Why Not JavaScript — the language choice that looks arbitrary until you see what hot reload and ahead-of-time compilation both demand from it, and why a framework that draws its own widgets could not have been written in the obvious language.

Editorial reflection

Writing this chapter I kept circling one thought: Flutter's central decision is not a technical preference, it is a claim about who should be responsible for what a button looks like. The wrapping approach says the platform should be, and that an app is a guest in someone else's house. The drawing approach says the product should be, and that a brand crossing two operating systems is more coherent than a brand deferring to each. Neither is correct in the abstract — they are different answers to a question about authority, and the engineering follows from the answer rather than producing it. What I find genuinely hard is that the costs arrive on wildly different schedules. The size floor you pay immediately and can measure. The drift you pay slowly, invisibly, and usually after the people who chose the framework have moved on, which makes it the sort of cost that never quite gets attributed to the decision that caused it. I do not think that is an argument against Flutter. I think it is an argument for writing the reasoning down at the time, so that whoever inherits the app knows the bill was expected.

Q&A · 0

Enrol to ask questions and join the discussion.

No questions yet — be the first to ask.