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.
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.
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.
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.
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.
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:
Edit here, then open the pad to run it on dartpad.dev.
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.
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.
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.
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
- Assuming Flutter wraps platform widgets. It draws its own; Cupertino calls no UIKit.
- Thinking Cupertino is "the native iOS look". It is Flutter's imitation, and its own docs say mimics.
- Expecting OS changes to arrive for free. A drawn widget updates when Flutter ships and you upgrade.
- Judging app size by growth rather than floor. The engine cost is fixed and paid on day one.
- Using Flutter for something tiny. The floor is the entire app when the app is small.
- Bolting one Flutter screen onto a native app. Costs are front-loaded; benefits compound with ownership.
- Forgetting that behaviour is drawn too. Scroll physics became Flutter's problem along with pixels.
- Treating Material and Cupertino as platform-locked. Either can be imported on either platform.
- Reading
flutter.jar's raw size as your APK. Release builds strip, tree-shake and split per ABI. - Carrying React Native's model across. There is no
UIViewunderneath; there is a canvas.
Interview Corner
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.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.
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
physicsis 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.
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.