1. Three trees in Flutter’s rendering mechanism
Working with Widgets in Flutter are two other partners: Elements and RenderObjects; Because of their tree-like structure, they are often referred to as three trees.
- Widgets: Widgets are the heart of Flutter and are immutable descriptions of the user interface. One of the things that Flutter works with the most is widgets, which hold up half of Flutter’s sky.
- Element: An Element is an instantiated Widget object that is generated using Widget configuration data at a specific location through the createElement() method of the Widget.
- RenderObject: It is used for layout and drawing of the application interface. It saves information such as the size and layout of elements.
2. Three trees at the first run
After getting to know three trees, how did Flutter create the layout? And how do they coordinate among the three trees? Let’s take a look at their synergy with a simple example:
class ThreeTree extends StatelessWidget { @override Widget build(BuildContext context) { return Container( color: Colors.red, child: Container(color: Colors.blue) ); }}
The example above is simple and consists of three widgets: Threetree, Container, and Text. So what happens when Flutter’s runApp() method is called?
When runApp() is called, the following events occur in the background for the first time:
Flutter builds a Widget tree containing the three Widgets;
Flutter walks through the Widget tree, then calls createElement() to create the corresponding Element objects based on the widgets in it, and finally assembles these objects into an Element tree.
Next, we create a third tree, which contains the RenderObject created by createRenderObject() with the Widget’s corresponding Element;
Below is the state of the Flutter after these three steps:
As you can see from the figure, Flutter created three different trees, one for Widget, one for Element, and one for RenderObject. Each Element has a corresponding reference to the Widget and RenderObject. You can say that an Element is a bridge between a tree of mutable widgets and a tree of immutable RenderObjects. Element is good at comparing two objects, which in Flutter are Widget and RenderObject. Its purpose is to configure the location of the Widget in the tree and keep references to the corresponding RenderObject and Widget.
The role of three trees
In short, for performance, to reuse Elements so that RenderObjects can be created and destroyed less frequently. Because it is expensive to instantiate a RenderObject, and frequent instantiation and destruction of RenderObject can have a significant impact on performance, Flutter uses the Element tree to compare the new tree to the original tree when the Widget tree changes:
//framework.dart @protected Element updateChild(Element child, Widget newWidget, dynamic newSlot) { if (newWidget == null) { if (child ! = null) deactivateChild(child); return null; } Element newChild; if (child ! = null) { assert(() { final int oldElementClass = Element._debugConcreteSubtype(child); final int newWidgetClass = Widget._debugConcreteSubtype(newWidget); hasSameSuperclass = oldElementClass == newWidgetClass; return true; } ()); if (hasSameSuperclass && child.widget == newWidget) { if (child.slot ! = newSlot) updateSlotForChild(child, newSlot); newChild = child; } else if (hasSameSuperclass && Widget.canUpdate(child.widget, newWidget)) { if (child.slot ! = newSlot) updateSlotForChild(child, newSlot); child.update(newWidget); assert(child.widget == newWidget); assert(() { child.owner._debugElementWasRebuilt(child); return true; } ()); newChild = child; } else { deactivateChild(child); assert(child._parent == null); newChild = inflateWidget(newWidget, newSlot); } } else { newChild = inflateWidget(newWidget, newSlot); } assert(() { if (child ! = null) _debugRemoveGlobalKeyReservation(child); final Key key = newWidget? .key; if (key is GlobalKey) { key._debugReserveFor(this, newChild); } return true; } ()); return newChild; } static bool canUpdate(Widget oldWidget, Widget newWidget) { return oldWidget.runtimeType == newWidget.runtimeType && oldWidget.key == newWidget.key; }
If the Widget in one location is inconsistent with the new Widget, you need to recreate the Element. If the Widget in one location is the same as the new Widget (the two widgets are equal or the runtimeType is equal to the key), then you only need to modify the configuration of RenderObject, instead of performing the costly instantiation of RenderObject. Because widgets are very lightweight and cost very little performance to instantiate, they are the best tool for describing the state of your APP (aka Configuration). Heavyweight RenderObjects (which cost performance to create) need to be created as little as possible and reused as much as possible; Does this make the Flutter APP look like a RecycleView?
Because elements are pulled out of the framework, you don’t have to deal with them very often. The context passed in each Widget’s build (BuildContext Context) method is the Element that implements the BuildContext interface.
4. Three trees when updated
Because widgets are immutable, when a Widget’s configuration changes, the entire Widget tree needs to be rebuilt. For example, when we change the color of a Container to orange, the framework triggers an action to rebuild the entire Widget tree. Because of the Element, Flutter compares the first Widget in the new Widget tree to the one before it. Next, compare the second Widget in the Widget tree to the one before it, and so on until the Widget tree comparison is complete.
class ThreeTree extends StatelessWidget { @override Widget build(BuildContext context) { return Container( color: Colors.orange, child: Container(color: Colors.blue,), ); }}
Flutter follows a basic rule of determining whether the new Widget and the old Widget are of the same type:
If it is not the same type, remove Widget, Element, and RenderObject from their trees (including their subtrees) and create new objects. If it is a type, just change the configuration in RenderObject, and then continue traversing down; In our example, the Threetree Widget is of the same type as the original one, and it’s configured the same way as the original Threetreerender, so nothing happens. The next node in the Widget tree is the Container Widget. Its type is the same as before, but its color has changed, so the RenderObject configuration will change accordingly, and then it will be re-rendered, leaving all other objects unchanged.
Notice the three trees. After the configuration changes, the Element and RenderObject instances remain unchanged.
This process is very fast, because the Widget’s immutability and lightweight allow it to be created quickly. In this process, the heavyweight RenderObjects remain unchanged until the corresponding type of Widget is removed from the Widget tree.
5. Three trees when the type of Widget changes
class ThreeTree extends StatelessWidget { @override Widget build(BuildContext context) { return Container( color: Color. orange, child: FlatButton(onPressed: () {}, child: Text(' three trees '),),); }}
As with the previous process, Flutter traverses down from the top of the new Widget tree to compare the Widget types in the original tree.
Since the FlatButton type is different from the corresponding Element type in the Element tree, the Flutter will remove this Element and its corresponding ContainerRender from the respective tree. The Flutter will then recreate the Element and RenderObject corresponding to the FlatButton.
When the new RenderObject tree is rebuilt, the layout is calculated and then drawn on the screen. Flutter uses a lot of optimizations and caching policies internally, so you don’t need to handle these manually.