Flutter Architecture Made it Easy
Flutter is a cross-platform UI toolkit that enables developers to write the code once and deploy it across multiple platforms, such as Android, iOS, Web, Windows, and Linux. Think of it as a universal screwdriver that can tackle any screw - on any surface. It aims to be a versatile tool whose goal is to build high-performance, platform-native feeling applications, embracing platform differences while maximizing code reuse. The development process leverages a VM to enable stateful hot reloads that instantly reflect code changes without recompilation. For release builds, however, the apps are compiled directly to platform-specific machine code, including Intel x64, ARM instructions, or JavaScript. For Android, Flutter's Embedder is written in Java and C++; Swift and Objective-C on iOS; and C++ on Windows and Linux. In this article, I’ll simplify the abstract concept of Flutter’s architecture, making it easy for everyone to understand. So, grab your cup of coffee (or tea) and some snacks, and join me on this journey! Architectural Layers Flutter's architecture is designed as a layered system, where independent libraries build upon each other, creating a flexible and extensible toolkit. Image 1: Architectural layers of Flutter. Source: https://docs.flutter.dev/resources/architectural-overview Layer 1: The Embedder – Platform-Specific At its lowest level, this layer is responsible for how Flutter applications will interact with the operating system (OS). It handles the crucial task of translating Flutter's instructions into a language the OS understands, making it look native to the eyes of the OS. It provides the entry point, coordinating with the OS for access to essential services like rendering, accessibility, and input, allowing Flutter code to be integrated as a module within existing native applications. Layer 2: The Engine – C/C++ Essentially, the Engine translates your code into the pixels you see on screen. It handles the rasterization of composited scenes for every new frame, providing low-level implementations of Flutter's core APIs, encompassing graphics, text layout, file access, and networking. It is accessible through the dart:ui library, which wraps the C++ code in Dart classes, exposing low-level primitives for graphics and text rendering. Layer 3: Framework – Dart The Framework, which is the layer we as developers interact with, comprises four inner layers, and it's a lot like playing with Legos. You start with the basic pieces and build them up step by step, each layer adding more complexity and functionality to your app. It is made of 4 inner-layers: Foundational Layer: This is like the Lego pieces you start with, simple and essential tools for creating everything. It gives you the building blocks for things like handling touches, drawing images, and adding animations. It provides low-level utilities like gestures, painting, and animation, like GestureDetector and CustomPainter. Rendering Layer: Once you have your pieces, this layer helps you figure out where to put them and how they should be arranged. It’s like designing the structure of your Lego creation, ensuring everything fits and looks the way you want. It allows developers to build a tree of render objects (more on this later), which defines how elements are drawn on the screen such as the RenderBox. Widget Layer: This is where you add the details to your Lego house, like windows and doors. Every piece in the rendering layer has a corresponding widget here, which defines the UI elements and how they should be put together. The example would be the Container – which is made of smaller widget such as LimitedBox, ContrainedBox, Allign, Padding, DecoratedBox, and Transform. Material Layer: Finally, this layer gives you pre-built Lego decorations and furniture. It provides ready-to-use UI components, like buttons and app bars, that you can easily add to your app without having to build them from scratch. These are high-level libraries that provide ready-to-use UI components, like MaterialApp and Scaffold. Image 2: Think of Flutter's layers like this: the Embedder is the translator, converting Flutter's instructions into the operating system's language. The Engine is like the artist painting on a canvas, rendering the visuals. The Framework is the architect, providing the building blocks for the application, which is like playing with Legos. Widgets and The Trees In the previous section, we explored Flutter’s Framework layer and how developers interact with it using building blocks similar to Lego pieces. These blocks, known as Widgets, are the core of Flutter development—hence the motto: Everything is a widget. Widgets serve as the fundamental UI components, nested together to build complex interfaces. From basic elements like text and buttons to advanced components like grids and sliders, everything in Flutter is a widget. This approach enhances flexibility
Flutter is a cross-platform UI toolkit that enables developers to write the code once and deploy it across multiple platforms, such as Android, iOS, Web, Windows, and Linux. Think of it as a universal screwdriver that can tackle any screw - on any surface. It aims to be a versatile tool whose goal is to build high-performance, platform-native feeling applications, embracing platform differences while maximizing code reuse.
The development process leverages a VM to enable stateful hot reloads that instantly reflect code changes without recompilation. For release builds, however, the apps are compiled directly to platform-specific machine code, including Intel x64, ARM instructions, or JavaScript. For Android, Flutter's Embedder is written in Java and C++; Swift and Objective-C on iOS; and C++ on Windows and Linux.
In this article, I’ll simplify the abstract concept of Flutter’s architecture, making it easy for everyone to understand. So, grab your cup of coffee (or tea) and some snacks, and join me on this journey!
Architectural Layers
Flutter's architecture is designed as a layered system, where independent libraries build upon each other, creating a flexible and extensible toolkit.
Image 1: Architectural layers of Flutter. Source: https://docs.flutter.dev/resources/architectural-overview
Layer 1: The Embedder – Platform-Specific
At its lowest level, this layer is responsible for how Flutter applications will interact with the operating system (OS). It handles the crucial task of translating Flutter's instructions into a language the OS understands, making it look native to the eyes of the OS. It provides the entry point, coordinating with the OS for access to essential services like rendering, accessibility, and input, allowing Flutter code to be integrated as a module within existing native applications.
Layer 2: The Engine – C/C++
Essentially, the Engine translates your code into the pixels you see on screen. It handles the rasterization of composited scenes for every new frame, providing low-level implementations of Flutter's core APIs, encompassing graphics, text layout, file access, and networking. It is accessible through the dart:ui library, which wraps the C++ code in Dart classes, exposing low-level primitives for graphics and text rendering.
Layer 3: Framework – Dart
The Framework, which is the layer we as developers interact with, comprises four inner layers, and it's a lot like playing with Legos. You start with the basic pieces and build them up step by step, each layer adding more complexity and functionality to your app. It is made of 4 inner-layers:
Foundational Layer: This is like the Lego pieces you start with, simple and essential tools for creating everything. It gives you the building blocks for things like handling touches, drawing images, and adding animations. It provides low-level utilities like gestures, painting, and animation, like GestureDetector and CustomPainter.
Rendering Layer: Once you have your pieces, this layer helps you figure out where to put them and how they should be arranged. It’s like designing the structure of your Lego creation, ensuring everything fits and looks the way you want. It allows developers to build a tree of render objects (more on this later), which defines how elements are drawn on the screen such as the RenderBox.
Widget Layer: This is where you add the details to your Lego house, like windows and doors. Every piece in the rendering layer has a corresponding widget here, which defines the UI elements and how they should be put together. The example would be the Container – which is made of smaller widget such as LimitedBox, ContrainedBox, Allign, Padding, DecoratedBox, and Transform.
Material Layer: Finally, this layer gives you pre-built Lego decorations and furniture. It provides ready-to-use UI components, like buttons and app bars, that you can easily add to your app without having to build them from scratch. These are high-level libraries that provide ready-to-use UI components, like MaterialApp and Scaffold.
Image 2: Think of Flutter's layers like this: the Embedder is the translator, converting Flutter's instructions into the operating system's language. The Engine is like the artist painting on a canvas, rendering the visuals. The Framework is the architect, providing the building blocks for the application, which is like playing with Legos.
Widgets and The Trees
In the previous section, we explored Flutter’s Framework layer and how developers interact with it using building blocks similar to Lego pieces. These blocks, known as Widgets, are the core of Flutter development—hence the motto: Everything is a widget. Widgets serve as the fundamental UI components, nested together to build complex interfaces. From basic elements like text and buttons to advanced components like grids and sliders, everything in Flutter is a widget. This approach enhances flexibility and reusability, as functionalities are implemented as widgets rather than mere properties.
Widgets, however, only serve as a blueprint or description of what the UI should look like, but don't do anything themselves. They are immutable. The actual work of creating and managing the UI is handled by other components. Flutter takes these widget configurations and creates corresponding Elements, which is an instantiation of a widget that exists in Flutter’s UI hierarchy. Each Element holds a reference to its associated widget and is responsible for creating and managing the underlying RenderObject, which in turn, is responsible for the actual layout, painting, and hit-testing of the UI. They dictate how visual elements are drawn and positioned on the screen.
Together, these components create Flutter’s powerful tree-based architecture, which consists of three interconnected trees: the Widget Tree, which defines the UI structure; the Element Tree, which manages widget instances; and the Render Tree, which controls the actual rendering and layout. Understanding how these trees interact is key to grasping Flutter’s reactive framework and efficient UI updates.
Image 3: The trees of Flutter: Widget Tree, Element Tree and RenderObjectTree
Widget Tree
It is a hierarchical arrangement of widgets that defines the structure and properties of the user interface. Since widgets are immutable, any changes to their parameters or state result in them being rebuilt with updated values. This structure follows a tree-like format, where each node represents a widget, establishing a parent-child relationship.
Element Tree
For every widget in the widget tree, Flutter creates a corresponding element, resulting in the element tree. These elements store information about the widget's parent, children, size, and its associated RenderObject. The element tree plays a critical role in maintaining the stability of the widget tree, managing the lifecycle of widgets and their corresponding RenderObjects. A key difference between widgets and elements is that elements are mutable and they are responsible for handling reconciliation, which involves comparing the widget's previous state with its updated state and propagating updates to its child.
RenderObject Tree
The RenderObject tree handles the actual layout and rendering of the UI. Flutter uses this tree to perform layout calculations and paint the screen. RenderObjects are mutable and manage the size, position, and rendering of their associated elements. Flutter performs layout by traversing this tree in a depth-first manner, passing size constraints from parent to child. Each child must adhere to the constraints set by its parent when determining its size. In response, the child reports its final size back to the parent while staying within the provided constraints. At the end of this single walk through the tree, every object has a defined size within its parent's constraints and is ready to be painted by calling the paint() method. The RenderObject Tree utilizes property caching, a mechanism that stores certain computed properties of a RenderObject, preventing the need for recalculating them each time the object is repainted.
Image 4: Representation of the three trees with their respective nodes. Source: https://docs.flutter.dev/resources/architectural-overview
This raises the question: why does Flutter have three separate trees? The answer lies in efficiency. When a layout change occurs, it's most effective to update only the necessary parts of the layout tree, specifically the RenderObject tree. If all trees were combined, updating the layout would require traversing many unnecessary nodes, slowing down the process. By maintaining separate trees, the Widget tree, Element tree, and RenderObject tree, Flutter effectively separates concerns within the UI rendering process - which enables efficient updates and optimal performance by reusing elements rather than rebuilding the entire interface. This separation is a fundamental aspect of Flutter’s high-performance rendering capabilities.
The separation of trees offers further benefits by creating a clearer division of responsibilities. Widgets can remain declarative and focused on configuration, while RenderObjects handle the complexities of rendering. This separation reduces complexity, lowers the risk of bugs, and simplifies testing. It also enhances layout safety: The RenderObject tree can enforce type correctness at runtime, ensuring, for example, that a RenderBox only receives child RenderObjects using box coordinates. Combining the RenderObject and Element trees would necessitate additional checks and complicate widget design, requiring each widget to be aware of its children's specific layout constraints and coordinate systems.
A Tree Example
Let’s say we are developing a simple Flutter application consisting of a Padding widget with a Text child. How would Flutter represent this in its internal tree structures? And what happens when the state changes?
Flutter starts by building the Widget Tree, by adding the Padding Widget
Next, Flutter creates an Element Tree, which acts as a bridge between widgets and their underlying rendering objects.
The SingleChildRenderObjectElement will then ask Flutter to create the RenderPadding, an object responsible for positioning and sizing its child with padding around it.
At this point, the Padding Widget will require its child and the cycle of creating element and renderObject restarts, but this time with the Text Widget.
At this point, our trees are complete. But what happens if the state changes to update the text from “Hello World” to “Hello Flutter”?
Flutter will examine the Element Tree and RenderObject Tree to determine the most efficient way to update the screen. Both the element and render object will assess which parts can be reused and which need to be replaced. Specifically, the TextElement will invoke the canUpdate() function (https://api.flutter.dev/flutter/widgets/Widget/canUpdate.html), which checks whether the old and new widgets share the same runtimeType, ensuring that only necessary updates are made.
static bool canUpdate(Widget oldWidget, Widget newWidget) {
return oldWidget.runtimeType == newWidget.runtimeType
&& oldWidget.key == newWidget.key;
}
If canUpdate() returns true
- The existing element remains in place.
- The old Widget is replaced, and the element updates its widget reference.
- If the widget contains properties that affect rendering (e.g., updated text, color, or padding), Flutter applies those changes to the existing RenderObject instead of recreating it.
Since the structure remains the same, only necessary parts of the UI are repainted, improving performance.
If canUpdate() returns false:
- Dispose of the old widget’s element and the element is removed from the tree.
- A new element and renderObject are created for the new widget.
The process involves disposal and recreation; it can be more performance-intensive than updating an existing element.
Conclusion
Flutter’s architecture, with its layered approach and tree-based structure, is designed to optimize performance, maintainability, and flexibility. By separating concerns into the Embedder, Engine, and Framework layers, Flutter ensures efficient rendering while embracing platform differences. The Widget, Element, and RenderObject trees further enhance this efficiency by enabling precise UI updates without unnecessary re-renders. Understanding these core concepts is key to mastering Flutter development, allowing developers to build high-performance, visually rich applications across multiple platforms. As we dive deeper into Flutter’s inner workings, it becomes clear why it has become a preferred choice for modern app development—offering both power and simplicity in a single framework.
Sources
- Flutter Engineering by Majid Hajian
- https://docs.flutter.dev/resources/architectural-overview
- https://api.flutter.dev/flutter/widgets/Widget/canUpdate.html