09 Widget the Foundation of Building Flutter Interfaces

09 Widget The Foundation of Building Flutter Interfaces #

Hello, I am Chen Hang.

In the previous articles on getting started with Flutter development and the basics of Dart, we learned about the overall architecture and basic principles of the Flutter framework. We analyzed the project structure and runtime mechanics of Flutter, and from a Flutter development perspective, we explored the basic design principles of the Dart language. We also gained a deep understanding of Dart’s syntax features through comparisons with other high-level languages.

These contents form the foundation for systematically learning how to build Flutter applications, helping us better grasp the core concepts and techniques of Flutter.

In the fourth article, “What are the key technologies that differentiate Flutter from other solutions?”, I shared an architecture diagram from the official Flutter documentation, where it can be seen that Widget is the foundation of the entire view description. This diagram is important, so I’m including it here once again.

Figure 1 Flutter Architecture Diagram

Note: This image is taken from the Flutter System Overview.

So, what exactly is a Widget?

A Widget is an abstract description of functionality in Flutter. It represents the configuration information of a view and also serves as a data mapping. It is the most fundamental concept in the Flutter development framework. Common terms in front-end frameworks such as view, view controller, activity, application, layout, etc., are all Widgets in Flutter.

In fact, the core design philosophy of Flutter is “everything is a Widget”. Therefore, when learning Flutter, the first thing we need to do is to learn how to use Widgets.

In today’s article, I will guide you to learn the design principles and basic principles of Widgets in Flutter, in order to help you gain a deeper understanding of the process of building views in Flutter.

Widget Rendering Process #

When developing an app, we often focus on how to organize view data in a structured way and provide it to the rendering engine to ultimately display the interface.

In most cases, different UI frameworks handle this issue in different ways, but they all use the concept of a view tree. Flutter extends the concept of the view tree and abstracts the organization and rendering of view data into three parts: Widget, Element, and RenderObject.

The relationship between these three parts is as follows:

Figure 2 Widget, Element, and RenderObject

Widget #

Widget is a structured description of the view in the Flutter world. You can think of it as a “control” or “component” in frontend development. Widget is the basic logical unit for implementing controls, and it stores configuration information about view rendering, including layout, rendering attributes, and event response information.

In terms of page rendering, Flutter takes the “Simple is best” concept to the extreme. Why do we say that? Flutter designs Widget as immutable, so when the configuration information for view rendering changes, Flutter chooses to rebuild the Widget tree to update the data. This simple and efficient way of building UI driven by data.

However, the downside of this approach is that it puts pressure on garbage collection due to the destruction and rebuilding of a large number of objects. However, since Widget itself does not involve actual rendering bitmaps, it is just a lightweight data structure with low reconstruction costs.

In addition, due to the immutability of Widget, rendering node reuse can be done at a relatively low cost. Therefore, there may be different Widgets corresponding to the same rendering node in a real rendering tree, which undoubtedly reduces the cost of rebuilding the UI.

Element #

Element is an instantiated object of Widget. It carries the context data for view construction and serves as a bridge connecting structured configuration information to the final rendering.

The Flutter rendering process can be divided into three steps:

  • First, create the corresponding Element tree from the Widget tree;
  • Then, create the corresponding RenderObject and associate it with the Element.renderObject property;
  • Finally, build the RenderObject tree to complete the final rendering.

As you can see, Element holds both Widget and RenderObject. Whether it is Widget or Element, neither is responsible for the final rendering. They only give orders and the real work is done by RenderObject. Now you might ask, since they are all giving orders, why do we need to add an intermediate Element tree? Why not let Widget directly command RenderObject to do the work?

The answer is that, yes, it can be done, but this will greatly increase the performance overhead of the rendering.

Because Widget is immutable, but Element is mutable. In fact, the Element tree abstracts the changes in the Widget tree (similar to React’s virtual DOM diff), and only synchronizes the parts that need to be modified to the real RenderObject tree, minimizing the modifications to the actual rendered view. This maximizes rendering efficiency and avoids destroying the entire rendered view tree and rebuilding it.

This is the purpose of the Element tree.

RenderObject #

From its name, we can intuitively know that RenderObject is primarily responsible for implementing the rendering of the view.

In the previous article “What is the key technology that differentiates Flutter from other solutions?” mentioned in the fourth article, we pointed out that Flutter creates different types of rendering objects for each control (Widget) in the control tree (Widget tree), forming a rendering object tree.

The rendering object tree in Flutter’s display process is divided into four stages: layout, painting, compositing, and rendering. The layout and painting are completed in RenderObject. Flutter uses a depth-first mechanism to traverse the rendering object tree, determine the positions and sizes of objects in the tree, and draw them on different layers. After the drawing is completed, the compositing and rendering work is handed over to Skia.

By introducing the concepts of Widget, Element, and RenderObject, Flutter divides the complex construction process from view data to view rendering into simpler and more direct parts, making it easier to manage and ensuring high rendering efficiency.

Introduction to RenderObjectWidget #

Through the introduction in the 5th article “Understanding how Flutter code runs on the native system starting from the standard template”, you should already know how to use StatelessWidget and StatefulWidget.

However, StatelessWidget and StatefulWidget are only containers used to assemble widgets, and they are not responsible for the final layout and rendering of components. In Flutter, the layout and rendering work is actually done in another subclass of Widget called RenderObjectWidget.

So, in this article, let’s take a look at the source code of RenderObjectWidget to see how to use Element and RenderObject to complete graphics rendering.

abstract class RenderObjectWidget extends Widget {
  @override
  RenderObjectElement createElement();
  @protected
  RenderObject createRenderObject(BuildContext context);
  @protected
  void updateRenderObject(BuildContext context, covariant RenderObject renderObject) { }
  ...
}

RenderObjectWidget is an abstract class. From the source code, we can see that this class has methods for creating Element, RenderObject, and updating RenderObject.

But, in reality, RenderObjectWidget itself is not responsible for creating and updating these objects.

For the creation of Element, Flutter will call createElement during the traversal of the widget tree to synchronize the configuration of the widget itself and generate the Element object for the corresponding node. And for the creation and updating of RenderObject, it is actually done in the RenderObjectElement class.

abstract class RenderObjectElement extends Element {
  RenderObject _renderObject;

  @override
  void mount(Element parent, dynamic newSlot) {
    super.mount(parent, newSlot);
    _renderObject = widget.createRenderObject(this);
    attachRenderObject(newSlot);
    _dirty = false;
  }
   
  @override
  void update(covariant RenderObjectWidget newWidget) {
    super.update(newWidget);
    widget.updateRenderObject(this, renderObject);
    _dirty = false;
  }
  ...
}

After the creation of the Element is complete, Flutter will call the mount method of the Element. In this method, the creation of the associated RenderObject object and the insertion into the render tree are completed, and the Element that is inserted into the render tree can be displayed on the screen.

If the configuration data of the widget changes, the Element node that holds the widget will also be marked as dirty. In the next rendering cycle, Flutter will trigger the update of the Element tree and use the latest widget data to update itself and the associated RenderObject object, and then enter the Layout and Paint processes. The actual drawing and layout process is completely done by RenderObject:

abstract class RenderObject extends AbstractNode with DiagnosticableTreeMixin implements HitTestTarget {
  ...
  void layout(Constraints constraints, { bool parentUsesSize = false }) {...}
  
  void paint(PaintingContext context, Offset offset) { }
}

After the layout and painting are complete, the work is handed over to Skia. It synthesizes the Bitmap directly from the render tree during VSync signal synchronization, and then submits it to the GPU. I have already explained this part in the previous article “What are the key technologies that differentiate Flutter from other solutions”. Here, I won’t repeat it.

Next, let me explain the relationship between Widget, Element, and RenderObject in the rendering process using the following interface example. In the example below, a Row container contains 4 child widgets, with an Image on the left and two Text widgets arranged in a Column container on the right.

Figure 3 Interface Example

So, after Flutter has traversed the widget tree and created the Element corresponding to each child widget, it also creates the RenderObject associated with it that is responsible for the actual layout and rendering.

Figure 4 “Three Trees” generated by the example interface

Summary #

Alright, that’s all for today’s introduction to the design ideas and basic principles of Widgets. Now, let’s review the main content of today’s session together.

Firstly, I introduced you to the rendering process of Widgets and we learned about the three core concepts of organizing and rendering view data in Flutter: Widget, Element, and RenderObject.

Widget is a structured description of views in the Flutter world, storing configuration information about view rendering. Element is an instantiated object of Widget, abstracting the changes in the Widget tree and synchronizing only the necessary parts to the actual Render Object tree, optimizing the process from structured configuration information to final rendering as much as possible. RenderObject is responsible for the final presentation of the view, achieving the display through layout and drawing.

Finally, after gaining some understanding of the Flutter Widget rendering process, I guided you through the code of RenderObjectWidget, helping you understand how the three objects - Widget, Element, and RenderObject - work together to accomplish the graphic rendering.

Now that you are familiar with the concepts of Widget, Element, and RenderObject, I believe you have a clear and complete understanding of the component rendering process. This will enable you to think about the rationality of framework design from different perspectives when learning about commonly used components and layouts in the future.

However, in daily development and learning, in most cases, we only need to understand the features and usage of various Widgets, without having to worry about Element and RenderObject. Flutter has already done a lot of optimization work for us, so we just need to complete the assembly and configuration of various Widgets in the upper-level code and leave the rest to Flutter.

Thought Question #

How do you understand the concepts of Widget, Element, and RenderObject? Do they correspond one-to-one? Can you find corresponding concepts in Android/iOS/Web?

Feel free to leave a comment in the comments section to share your thoughts. I’ll be waiting for you in the next article! Thank you for listening, and feel free to share this article with more friends to read together.