Flutter
Flutter is Google's answer to building for several platforms from one codebase, and it takes the opposite approach to React Native: instead of asking the operating system for its widgets, Flutter ships a rendering engine and draws every pixel itself. That one decision explains the consistency, the performance, the size of the binary and the places the approach struggles. This page covers the rendering pipeline, Dart, state management, and where Flutter fits.
- Rendering
- Own engine, own pixels
- Language
- Dart, AOT-compiled
- Platform APIs
- Over a channel
Flutter draws the pixels itself
When you write a button in Flutter, no UIButton and no Android Button is created. Flutter builds its own tree, lays it out itself, and hands the result to an engine that rasterises it onto a canvas on the GPU. The platform provides a surface to draw on and very little else. This is the single fact that explains almost everything else about the framework.
The pipeline runs through three trees, and knowing which is which makes the framework's behaviour legible. Widgets are your description — immutable, cheap, thrown away and rebuilt constantly. Elements are what is actually mounted, and they persist across rebuilds; this is why a widget can be recreated sixty times a second without recreating its state. Render objects do the layout and painting, and they are the expensive ones, which is why Flutter works hard to reuse them.
What Flutter cannot draw, it has to ask for. Camera, biometrics, notifications, Bluetooth, file pickers, in-app purchases — all of it is reached over a platform channel, which is an asynchronous message to Swift or Kotlin code. In practice a package already exists for the common ones, but the dependency is real: each of those packages is native code somebody has to maintain against two evolving platforms. Accessibility and text input work, but they are bridged into the platform's services rather than inherited from a native widget, which is why they occasionally need attention that a native app would not.
Dart, and why it is not a drawback
Learning a language for a framework sounds like a cost, and Dart is the smallest version of that cost you could ask for: anyone comfortable with TypeScript, Java or C# reads it on the first afternoon. It is statically typed, sound about null safety — a nullable value cannot be used as a non-nullable one, and the compiler enforces it — and single-threaded per isolate, with async / await that will look familiar.
What makes Dart unusual is that it compiles two different ways for two different purposes. In development it runs on a virtual machine with just-in-time compilation, which is what makes stateful hot reload possible — you change a widget and the running app updates in under a second without losing where you were. For release it compiles ahead of time to native machine code, so there is no interpreter and no JavaScript bridge in the shipped app. Development speed and release performance usually trade against each other; here they do not.
Concurrency works like Node: one thread per isolate, with an event loop, so await never blocks the UI but a long computation does. For genuinely heavy work you spawn another isolate, which has its own memory and communicates by message rather than by shared state. It is more ceremony than a thread and it removes the entire category of data races that threads bring.
State management, and the honest answer
Flutter has no official state management solution, which is why every discussion about it turns into a comparison of packages. The useful way to see the field is by how much structure each one imposes, and the right answer depends far more on the size of the team than on the merits of the library.
setState
Built in, local to one widget. Correct for anything a single screen owns, and wrong the moment two screens need the same value.
Provider / Riverpod
Dependency injection plus observable state. Riverpod is the modern of the two: compile-time safe, testable, and independent of the widget tree.
BLoC
Events in, states out, as streams. Heavy ceremony for a small app; predictable and traceable for a large team on a long project.
The advice that holds regardless: keep state as close to where it is used as it can live, and move it outward only when a second place genuinely needs it. Most of the complexity people attribute to Flutter state management is self-inflicted — a global store introduced on day one for an app that has four screens and no shared state to speak of.
How this shows up in real delivery
Two costs are worth knowing before the first release. The engine ships inside the app, so a Flutter binary starts several megabytes larger than a native one — irrelevant for most products, and a real problem in markets where download size affects installs. And Flutter on the web is technically supported but renders into a canvas, so text selection, SEO and accessibility behave unlike a normal page; it suits an internal tool or an app-like product, not a public content site.
On the positive side, the tooling is genuinely a strength. Hot reload changes how UI work feels — you iterate on spacing and colour in seconds rather than in build cycles. Flutter DevTools shows the widget tree, the rebuild count per widget and a frame timeline that separates UI work from raster work, which means "why is this janky" is a question with an answer rather than a guess. Widget tests run without a device and are fast enough to keep in a pre-commit hook.
Where it degrades
- A
buildmethod hundreds of lines long, which rebuilds a whole screen for one changed value. - Widgets extracted into methods rather than classes, so
constcan never skip them. - A global state package adopted on day one for an app with no shared state to manage.
- Heavy computation on the UI isolate, which drops frames exactly as a blocked event loop would.
- One design shipped to both platforms with no thought for either one's conventions.
- Native-code packages adopted without checking who maintains them against new OS versions.
- Flutter web chosen for a public, content-heavy site, where a canvas is the wrong output.
When to use it
Use it when
- Products that must look and behave identically on both platforms, from one design specification.
- Custom, brand-heavy interfaces, where you were going to override the platform widgets anyway.
- Animation-rich UI — owning the rendering pipeline is exactly the advantage here.
- Teams without existing React skill, for whom Dart is no more of a hurdle than any other new stack.
Avoid it when
- Apps expected to feel exactly like the platform, where inherited native widgets are the point.
- Products where download size decides installs, since the engine travels with the binary.
- Public, content-heavy websites — the canvas renderer is the wrong tool for text and SEO.
- Anything built around a bleeding-edge platform API, which reaches Flutter after it reaches native.
Found this useful?
Share it with someone who is working on the same problem.