Flutter’s Architecture: Understanding the Internals
If you’re looking for the best Flutter app development company for your mobile application then feel free to contact us at — support@flutterdevs.com.
Table of Contents
Overview of Flutter’s Architecture
Flutter Engine: The Heart of Rendering
The Flutter Rendering Pipeline: How the UI is Drawn
Skia: The Graphics Engine Behind Flutter
Dart and Skia: How They Communicate
Deep Dive into Flutter’s Rendering Pipeline
Layout Phase: Determining Size and Position
Paint Phase: Rendering Visuals to the Screen
Compositing: Combining Layers for Efficient Rendering
Introduction
Flutter has quickly become one of the most popular frameworks for building cross-platform mobile apps. With its rich widget ecosystem, beautiful UIs, and seamless performance across iOS and Android, Flutter is an attractive choice for developers. However, behind the scenes, Flutter’s architecture is based on a highly optimized and custom rendering pipeline that ensures it runs efficiently and provides developers with great flexibility.
In this post, we will examine Flutter’s internal architecture, focusing on its engine, rendering pipeline, and widget drawing on the screen. We will also discuss how Dart interacts with Skia, Flutter’s graphics library, and the custom rendering system that powers its high performance.
Overview of Flutter’s Architecture
Flutter’s architecture comprises several layers that work together to provide an efficient, high-performance development experience. Let’s start with a quick overview of the key components:
- Dart App: It composes widgets into the desired user interface (UI) and implements the business logic necessary for the application. This involves arranging the widgets in a way that meets the design requirements and ensuring the app’s functionality is driven by the appropriate business rules and processes.
- Flutter Framework (Dart-based): This is the top layer of Flutter where developers work directly. It includes all the widgets, libraries, and tools to create the app’s UI and business logic. The framework itself is written in Dart, which provides an expressive, easy-to-learn programming environment.
- Flutter Engine: This is the core engine written in C++ that powers Flutter’s rendering capabilities. It handles low-level operations such as rendering, input handling, text layout, and more. The engine communicates with the underlying platform (iOS/Android) through platform channels to access native features.
- Platform Layer: This layer provides access to the device’s platform-specific features such as sensors, cameras, geolocation, etc. Flutter communicates with this layer using platform channels, which allow the Dart code to interact with native code (Java/Kotlin for Android and Swift/Objective-C for iOS).
- Embedder: The system coordinates with the underlying operating system to gain access to essential services, such as rendering surfaces, accessibility, and input management. It also takes responsibility for managing the event loop, ensuring that events are processed efficiently. Additionally, the system exposes a platform-specific API that allows the Embedder to be integrated seamlessly into applications, enabling smooth interaction with the operating system’s services and resources.
- Runner: It composes the components exposed by the platform-specific API of the Embedder into an app package that is runnable on the target platform. This process is an integral part of the app template generated by the
flutter create
command, ensuring that the necessary resources and configurations are in place for the app to function properly on the specified platform.
Flutter Engine: The Heart of Rendering
The Flutter Engine is the core of Flutter’s performance and rendering. It is written in C++ and is responsible for managing the entire lifecycle of a Flutter app. The engine provides several essential components, including:
- Dart VM: Flutter runs Dart code using the Dart Virtual Machine (VM). This VM is highly optimized for Flutter and enables efficient execution of business logic. During development, Flutter uses Just-In-Time (JIT) compilation to speed up the development cycle. In production, Ahead-Of-Time (AOT) compilation is used for optimized performance.
- Skia Graphics Library: Skia is a 2D graphics library used by Flutter to draw everything from simple shapes, text, and images to complex animations. Skia provides hardware-accelerated rendering, making it ideal for Flutter’s performance-oriented needs.
- Text Layout: Flutter uses Skia’s text layout engine to render text across various fonts, languages, and styles. It includes features such as font rasterization and text shaping.
- Platform Channels: Flutter’s engine also manages communication with native platform code through platform channels, allowing Flutter apps to access native features like camera, geolocation, and more.
The Flutter Rendering Pipeline: How the UI is Drawn
One of the most interesting aspects of Flutter’s architecture is its custom rendering pipeline. Unlike many mobile UI frameworks, Flutter does not rely on the native platform’s rendering system. Instead, it builds its UI using its own set of widgets, which it then renders via Skia. Let’s walk through the rendering pipeline step by step:
1. Widget Creation
In Flutter, everything is a widget. Widgets are the building blocks of the UI, and they describe what should appear on the screen. However, widgets themselves do not draw anything. They are immutable and declarative, meaning that when a widget is updated, it results in a new widget tree being created.
Text("Hello, Flutter!", style: TextStyle(fontSize: 24, color: Colors.blue));
This widget does not actually paint anything on the screen. It simply describes that there should be text with the specified style. Flutter uses a widget tree to manage the app’s UI, where each widget represents a part of the UI (like text, images, buttons, etc.).
2. Element Tree
Widgets are lightweight and immutable. When a widget changes, it doesn’t modify the existing widget directly; instead, it creates a new widget. The Flutter framework converts this widget tree into an Element tree.
An Element is a runtime representation of a widget and holds references to the widget’s state. This allows Flutter to keep track of the UI and the state of each widget efficiently.
3. RenderObject Tree
Once the element tree is established, Flutter transforms it into a RenderObject tree. RenderObjects are the low-level objects responsible for laying out and painting widgets on the screen. They handle the layout of widgets in terms of size and position.
The RenderObject tree can be thought of as a more detailed version of the widget tree, where each object has the responsibility for rendering itself. For instance, a Container
widget might be represented as a RenderBox
object.
Container(
padding: EdgeInsets.all(8.0),
child: Text("Hello, Flutter!"),
)
In this example, the Container
widget would have a corresponding RenderBox
that handles the layout and painting of the text and the padding inside.
4. Layout Phase
The layout phase is responsible for determining the size and position of every widget. It starts at the root of the RenderObject
tree and moves downward, recursively calculating the layout of each widget.
In the layout phase, each RenderObject
must decide its size (width and height) and its position relative to its parent and children. Flutter’s layout system is highly flexible, allowing widgets to behave in a variety of ways (e.g., wrapping, expanding, or aligning).
Here’s an example of a Column
widget containing two children
Column(
children: [
Text("First Item"),
Text("Second Item"),
],
)
During the layout phase, the Column
will calculate the size and position of the two Text
widgets, aligning them vertically.
5. Paint Phase
Once the layout phase is completed, Flutter enters the paint phase. This is when the actual drawing happens. In this phase, each RenderObject
will paint itself onto the screen using the Canvas API.
For example, the Text
widget will use Skia’s text rendering capabilities to draw the text, while a Container
might use a rectangle to render its background.
renderBox.paint(canvas, offset);
In this code, renderBox
is the RenderObject
associated with the widget, and paint()
is responsible for drawing the widget onto the screen.
6. Compositing
Once the widgets are painted, Flutter may need to combine the rendered layers into a single image that can be displayed on the screen. This process is called compositing.
Flutter uses a layered compositing system to combine multiple layers of UI elements. This allows Flutter to optimize the rendering process, ensuring that only the parts of the screen that have changed need to be redrawn.
For example, if a Container
widget changes, only that widget’s layer will be re-rendered, not the entire screen.
Layers
Layers are an important concept of the Flutter framework, which are grouped into multiple categories in terms of complexity and arranged in the top-down approach. The topmost layer is the UI of the application, which is specific to the Android and iOS platforms. The second topmost layer contains all the Flutter native widgets. The next layer is the rendering layer, which renders everything in the Flutter app. Then, the layers go down to Gestures, foundation library, engine, and finally, core platform-specific code. The following diagram specifies the layers in Flutter app development.
Skia: The Graphics Engine Behind Flutter
One of the central pillars of Flutter’s performance and rendering capabilities is Skia, an open-source 2D graphics library that powers much of Flutter’s graphical rendering. Skia provides an efficient and flexible framework for drawing complex visual elements such as text, images, shapes, and animations on the screen. It is a low-level graphics engine used by both Android and Chrome, but Flutter leverages it as its primary means of rendering UI content. Skia’s high-performance rendering system is one of the main reasons Flutter apps can run so smoothly across platforms.
Key Features of Skia in Flutter
Here are the most crucial features of Skia, which are leveraged by Flutter to render high-performance graphics:
- Cross-Platform Rendering: Skia is platform-agnostic, meaning it can be used across various platforms, including Android, iOS, macOS, Linux, and Windows. Since Flutter relies on Skia for rendering, it can ensure consistent visual fidelity across all platforms.
- Low-Level Graphics API: Skia provides a low-level API to interact with 2D drawing primitives like paths, text, and bitmaps. Flutter interacts with Skia using this API to render widgets and layouts to the screen efficiently.
- GPU Acceleration: One of the most important aspects of Skia’s performance is its ability to leverage GPU acceleration for rendering tasks. Skia uses OpenGL, Vulkan, Metal, and other platform-specific graphics APIs to offload rendering to the GPU, which makes rendering smoother and more efficient, particularly when dealing with animations and complex visual effects.
- Vector Graphics: Skia supports vector graphics, which means that the visual elements drawn on the screen can be scaled up or down without losing quality. This is particularly important for Flutter apps, as it ensures that the UI looks crisp on devices with varying screen sizes and resolutions.
- High-Performance Text Rendering: Skia comes with a robust text rendering engine capable of high-performance text layout, including the ability to handle complex scripts, kerning, text alignment, and font rasterization. Flutter uses Skia’s text rendering capabilities for all its text-based widgets, ensuring that text is drawn accurately and efficiently.
- Image Rendering: Skia is responsible for rendering images in Flutter as well. Whether it’s raster images (like PNGs, JPEGs) or vector images (like SVGs), Skia is used to efficiently display these images with high performance. It also supports hardware-accelerated image manipulation, which makes operations like image scaling and transformations fast and efficient.
Skia handles the low-level drawing operations, while Flutter’s engine controls the rendering pipeline, managing the communication between the Dart code and Skia.
Dart and Skia: How They Communicate
Flutter’s use of Dart for the business logic of the application and Skia for rendering might seem like a disconnect, but thanks to the Flutter engine, these two layers work together seamlessly. The engine acts as a bridge between the high-level Dart code and the low-level Skia rendering commands.
When you write your Flutter app using Dart, you’re primarily working with Widgets that describe the structure and behavior of your UI. However, once the widget tree is constructed, the Flutter engine translates these high-level descriptions into low-level rendering commands that Skia can process.
The Dart VM and Skia work together to make sure Flutter’s rendering pipeline runs smoothly. The Dart code, running in the Dart VM, is responsible for creating the widget tree and managing the app’s state. When a widget needs to be drawn, the Dart code interacts with the Flutter engine, which then communicates with Skia to render the widget to the screen.
Dart communicates with Skia through the Flutter engine, which acts as a bridge. The engine converts the high-level widget description in Dart into low-level drawing commands that Skia can understand. Skia, in turn, takes care of the actual drawing operations, using GPU resources when available to speed up rendering.
This separation allows Flutter to remain efficient while providing developers with a high-level declarative API to build UIs. By abstracting away the details of low-level rendering, Flutter enables developers to focus on building beautiful and performant apps without needing to worry about graphics rendering details.
How Dart and Skia Communicate:
- Dart VM: When you write Dart code, it’s compiled by the Dart VM. During development, Just-In-Time (JIT) compilation is used, which makes hot-reloading possible. In production, Ahead-Of-Time (AOT) compilation ensures faster startup times and better performance.
- Flutter Engine: The engine contains a C++ layer that acts as an intermediary between Dart and Skia. It translates the layout and painting instructions from Dart into commands that Skia can process.
- Skia: Skia is where the actual drawing happens. It interacts with the hardware through GPU APIs (like OpenGL, Vulkan, and Metal) to render the graphics and text to the screen. The Canvas API in Flutter acts as a wrapper around Skia’s lower-level functions, allowing you to draw shapes, images, and text.
For instance, when you call a setState()
method in your Flutter app, which causes the widget tree to rebuild, Flutter’s engine will rebuild the RenderObject tree, and any changes to the layout or appearance of the UI will be communicated to Skia to be redrawn.
For example, consider the following Flutter widget that draws a circle:
CustomPaint(
size: Size(200, 200),
painter: CirclePainter(),
)
The CirclePainter
class would extend the CustomPainter
class and override the paint
method to draw the circle using Skia’s Canvas
:
class CirclePainter extends CustomPainter {
@override
void paint(Canvas canvas, Size size) {
final paint = Paint()
..color = Colors.blue
..style = PaintingStyle.fill;
canvas.drawCircle(Offset(size.width / 2, size.height / 2), 50, paint);
}
@override
bool shouldRepaint(CustomPainter oldDelegate) {
return false;
}
}
Example: Flutter and Skia for Custom Drawing
Consider a scenario where you want to draw a custom shape (say, a custom circle) using Flutter’s CustomPainter
class. The CustomPainter
allows you to use Skia’s rendering API to draw shapes directly onto the screen. Here’s an example of how you might do this:
class CirclePainter extends CustomPainter {
@override
void paint(Canvas canvas, Size size) {
final paint = Paint()
..color = Colors.blue
..style = PaintingStyle.fill;
// Drawing a circle using Skia's Canvas API
canvas.drawCircle(Offset(size.width / 2, size.height / 2), 50, paint);
}
@override
bool shouldRepaint(CustomPainter oldDelegate) {
return false; // We don't need to repaint unless the custom painter logic changes
}
}
class MyCustomPaintWidget extends StatelessWidget {
@override
Widget build(BuildContext context) {
return CustomPaint(
painter: CirclePainter(),
size: Size(200, 200), // Size of the area to paint
);
}
}
In this example:
- CustomPainter extends
CustomPainter
and overrides thepaint()
method, which is called to render the widget. - Canvas is used to draw the circle. Internally, the
drawCircle
method uses Skia’s API to paint the circle onto the screen. - The
Paint
object specifies the color and the style of the drawing.
Skia is at the heart of this process, enabling Flutter to draw the custom shapes using optimized, low-level rendering routines.
Deep Dive into Flutter’s Rendering Pipeline
Now, let’s go even deeper into understanding how Flutter’s rendering pipeline works and how it helps Flutter achieve its high performance. The rendering pipeline is the series of steps that take place after the widget tree is built and before the app’s UI is displayed on the screen. This process allows Flutter to efficiently transform widget descriptions into pixel-perfect visuals, and it involves key components like Widget Trees, Element Trees, RenderObject Trees, Layout, Painting, and Compositing.
Each of these steps plays a crucial role in ensuring that Flutter apps perform well, even when complex UIs and animations are involved.
Widget Tree to RenderObject Tree: The Transformative Process
The journey from a simple Text
widget to a rendered element on the screen starts with the widget tree. Each widget defines part of the UI (e.g., a button, text field, etc.). When a widget is placed inside another widget, Flutter’s framework begins by converting the widget tree into an Element tree and then into a RenderObject tree.
Widget Tree
The widget tree is a hierarchy of Flutter widgets that are declarative descriptions of the UI. Widgets are lightweight and immutable, meaning that when a widget changes, it creates a new widget. For example, if the text in a Text
widget changes, the widget tree is recreated with the new text value.
Element Tree
The Element tree is a more efficient representation of the widget tree that exists at runtime. Each Element corresponds to a widget in the widget tree and stores references to its state. Unlike widgets, which are immutable, elements can have state, making them ideal for managing the UI’s changes during execution.
RenderObject Tree
The RenderObject tree is the most crucial part of the rendering pipeline. The RenderObject represents the visual elements that Flutter needs to draw on the screen. Every RenderObject has properties such as size, position, paint, and layout constraints that help it define how the widget should appear and behave. The RenderObject tree is responsible for rendering the app’s UI and handles tasks such as layout and painting.
Layout Phase: Determining Size and Position
The layout phase of the Flutter rendering pipeline determines the size and position of each widget on the screen. During this phase, each RenderObject must figure out its size based on its parent’s constraints and the available space in the layout.
The layout phase is hierarchical. It starts from the root of the tree and recursively processes each child. Here’s how the layout process works:
- Parent Constraints: The parent widget gives constraints to its child. For example, if you have a
Row
widget with twoText
widgets inside, theRow
widget determines how much space the child widgets can occupy. - Size Determination: Each child widget (RenderObject) will calculate its size based on these constraints. For example, if the parent widget is a
Column
, it might impose constraints that tell its children to expand vertically, or if the parent is aRow
, the children might be constrained to expand horizontally. - Positioning: After each child widget determines its size, Flutter positions the widgets in the layout tree by calculating their offsets (x and y positions). This ensures that each widget is placed correctly on the screen relative to its parent.
In a typical Column
widget, for example, each child is positioned vertically, with padding and spacing taken into account during the layout phase.
Column(
children: [
Padding(
padding: EdgeInsets.all(10),
child: Text("Hello, Flutter!"),
),
RaisedButton(
onPressed: () {},
child: Text("Press Me"),
),
],
)
Here, during the layout phase, the Column
widget will position each child vertically, taking into account the padding for the first Text
widget and the space needed for the RaisedButton.
Paint Phase: Rendering Visuals to the Screen
The paint phase is when the actual drawing happens. After the layout phase has calculated where each widget should be positioned, it’s time to paint the widgets. This phase utilizes the Canvas API, which is where Skia comes into play.
The Canvas
is a central component of Flutter’s rendering system, and it provides methods to draw shapes, text, images, and more. Each RenderObject
can paint itself on the canvas by calling specific methods.
For example, a simple Container
widget might use a RenderBox
to draw a rectangle, while a Text
widget might use a text drawing method.
Example: Custom Painter for Drawing Shapes
Let’s look at a basic example where we use a CustomPainter
to draw a shape (circle) on the screen.
class CirclePainter extends CustomPainter {
@override
void paint(Canvas canvas, Size size) {
final paint = Paint()
..color = Colors.blue
..style = PaintingStyle.fill;
canvas.drawCircle(Offset(size.width / 2, size.height / 2), 50, paint);
}
@override
bool shouldRepaint(CustomPainter oldDelegate) {
return false;
}
}
In this example, the CirclePainter
class extends CustomPainter
. The paint
method is where the actual drawing happens. Here, we are using Skia’s canvas.drawCircle
method to draw a blue circle at the center of the given size
. This is an example of how Flutter uses the Canvas
API to interact with Skia to render graphics on the screen.
Compositing: Combining Layers for Efficient Rendering
Once the widgets are painted, Flutter enters the compositing phase, where it combines different layers of the UI into a single image to be displayed on the screen. Flutter’s compositing system ensures that only the parts of the screen that need to be updated are redrawn.
In this phase, Flutter uses a layered compositing model, where each widget’s painted output is stored in a layer. These layers are then composited into a single image that will be displayed on the screen. The advantage of this approach is that Flutter only re-renders the portions of the screen that have changed, rather than redrawing the entire screen.
For instance, if a button changes color on tap, only the button’s layer needs to be updated, not the entire screen. This results in significant performance gains, especially in complex applications with many UI elements.
Here’s an example of how layers can be used for custom painting with CustomPaint
:
CustomPaint(
painter: CirclePainter(),
)
When Flutter draws the CirclePainter
, the painted content is stored in a layer. If this layer needs to be updated (for instance, if the circle’s position or color changes), only this layer is recomposited. This makes Flutter apps extremely efficient when updating the UI.
Conclusion
Understanding the internals of Flutter’s architecture, its custom rendering pipeline, and the relationship between Dart and Skia is crucial for developers aiming to fully leverage Flutter’s capabilities. Flutter’s approach to UI rendering, with a custom pipeline based on Skia, enables it to deliver high performance and consistent results across platforms. This architecture not only makes Flutter fast and flexible but also empowers developers to create sophisticated UIs with full control over rendering.
By learning about the engine, the rendering process, and how Flutter draws widgets on screen, you can optimize your Flutter apps further, troubleshoot performance issues, and even build custom rendering solutions when needed.
At What the Flutter, we love working with Flutter apps to make them high-performing that delight users. Contact us today to discuss how we can optimize your Flutter app performance.
❤ ❤ Thanks for reading this article ❤❤
If I got something wrong? Let me know in the comments. I would love to improve.
Clap 👏 If this article helps you.
References:
Flutter architectural overview
A high-level overview of the architecture of Flutter, including the core principles and concepts that form its design.docs.flutter.dev
Flutter’s Architecture Explained
We explore the intricacies of Flutter’s architecture, divided into three primary layers: the Framework, the Engine, and…somniosoftware.com
Feel free to connect with us:
And read more articles from FlutterDevs.com.
FlutterDevs team of Flutter developers to build high-quality and functionally-rich apps. Hire a Flutter developer for your cross-platform Flutter mobile app project hourly or full-time as per your requirement! For any flutter-related queries, you can connect with us on Facebook, GitHub, Twitter, and LinkedIn.
We welcome feedback and hope that you share what you’re working on using #FlutterDevs. We truly enjoy seeing how you use Flutter to build beautiful, interactive web experiences.