This chapter is about the basic principles of Flutter technology. After understanding the underlying principles, we can better expand the technical fields of Flutter, such as state management, Navigator page navigation, Key design principle, FPS and so on. We will start with the widgets that are most commonly used in Flutter development. Parse the Flutter Widget architecture and UI rendering process. It takes about 2 minutes to read this article.
Consider a few questions
- How does setState update the UI?
- What is the relationship between Widget, Element, and RenderObject trees? What is Layer Tree? Why do we have so many trees?
- Theme Change Why can global skin change?
The main module
To answer the above questions, we answer them around the following modules:
- Categories of widgets.
- State Life cycle.
- Widget, Element, RenderObject + LayerTree
- UI update and rendering process.
- InheritedWidget data sharing principles.
Classification of the Widget
Statefulwidgets, StatelessWidgets, proxyWidgets, and RenderObjectWidget inherit from the Widget base class, which together form the Widget skeleton.
- StatelessWidget a StatelessWidget. Common subcategories are Text and Container.
- StatefulWidget a StatefulWidget. The common subclasses are Image and Navigator.
The question here is what is a state? Widgets are designed to be immutable under the Flutter architecture. Normally, a new Widget object will be rebuilt every frame without knowing its previous state. StatefulWidget saves State by associating it with a State object.
For example, a button has two states: Normal and pressed. When different styles are displayed during interaction with the user, the style switching is actually a state update process, and the current gesture interaction is recorded inside the button. Otherwise, it is stateless, such as the Text in the class diagram. As long as the Text and style are given, the Text will not change.
You might ask, shouldn’t images in that kind of graph also be stateless? Image is a special Widget that provides interfaces to Image placeholders, default images, and error images. In fact, it is the state changes that can occur during the presentation of an Image.
- RenderObjectWidget implements layout and Paint.
Three subclasses can be derived according to the number of child widgets it receives:
A subclass | instructions |
---|---|
LeafRenderObjectWidget | Leaf node, which only provides paint capabilities, such as RawImage. RawImage is responsible for all the drawing work inside an Image. |
SingleChildRenderObjectWidget | Widgets that accept only a single child, such as Opacity, change the child’s transparency. |
MultiChildRenderObjectWidget | Provides layout capabilities for widgets that accept multiple children, such as Stack for cascading layout, Flex for linear layout, and the common subclasses Row and Column. |
- ProxyWidget is a ProxyWidget that can quickly trace the parent node and is usually used for data sharing. Common subclasses InheritedWidget and various state management frameworks such as flutter_redux and provider are implemented based on it, which will be introduced in detail in the following chapters. MediaQuery: read device window, screen information; _InheritedTheme: A wrapper class inside the Theme that processes themes globally in the APP.
To sum up:
Statefulwidgets and StatelessWidgets are more like composite widgets that can be used to build complex upper-level interfaces. RenderObjectWidget specific implementation layout, paint work; Proxywidgets are commonly used for data sharing.
State lifecycle
We said that a StatefulWidget is a StatefulWidget, so where is its state stored? We say that a Widget describes only the properties of a control, but why can its state be preserved?
Simply put, State is stored in the abstract class State, which depends on the creation of the corresponding StatefulElement, which is a subclass of Element. Finally, StatefulElement holds a reference to State. What is Element? We’ll talk about that later.
Sound a little confused? A class diagram helps tease out the internal structure of State.
To sum up:
State holds references to the current Widget and Element, which is why we can access Widget objects inside State; We can also jump to pages using the context object, which is “==” _element. Finally, we need to implement the Build abstract method to create the corresponding Widget object.
The flow chart
The entry point for the process is defined as element.inflateWidget, and the usual entry point is when the Widget is first loaded onto the page. This is covered in the Widget update rendering mechanism, which we’ll skip for now.
State that
- CreateElement A StatefulElement is created when a new StatefulWidget needs to be loaded into the page.
- CreateState is called once in the StatefulElement constructor, creating the state object and assigning the created Element object to the state _Element object. When the element is mounted to the Element Tree, state is mounted. The setState method is called in the Mounted state.
- The _firstBuild method is called once inside the initState mount method, followed by a call to state. initState to initialize the State.
- DidUpdateWidget When the widget configuration changes, the next render frame after setState is called is called.
- DidChangeDependencies Is called when there is a state change in the parent widget of the InheritedWidget type on which this widget depends, and once after the initState call ends.
- The build method is called for the first time by calling _firstBuild during mount. In addition, as we’ll see later, when a widget needs to be redrawn, its parent widget calls its build method first, and the returned newWidget is compared with the oldWidget to determine whether to reuse or use the new Element object.
- Deactivate When updateChild finds an element that needs to be rebuilt, it calls its deactivateChild first, and internally places the element in _InactiveElements. After the current frame is drawn, If the Element is not reused, the _unmountAll method of _InactiveElements is called, and finally the Dispose method of Element’s corresponding state is called.
- Dispose can release resources. After super. Dispose is executed, the state is unmounted and the resource release action should be before super.
Bad Case
After getting familiar with the life cycle of the state above, let’s look at a bad case, which is also prone to be made by beginners on the road:
At first glance, this might seem like a good thing, but currentState doesn’t change once it’s assigned, because its initialization code is in initState, and initState is only called once during state changes, As soon as the upper-layer component that DemoPage depends on changes and a new currentState value is passed in, the bug starts ~
✅ The correct way to do this is:
- State is held internallyThe latestReferences to widgets can be declared directly where currentState is needed
widget.currentState
. - Another option is to overwrite the didUpdateWidget method and assign currentState again.
Widget, Element and RenderObject trees
Let’s have a preliminary understanding of the three concepts:
- A Widget represents the view information of a small control. Designed to be immutable under Flutter, a Widget has no state storage capability of its own. It can be understood as a configuration file that can be used immediately and created with little overhead.
- RenderObject objects with real layout and drawing capabilities, such as the Image Image control, will eventually create the RenderImage for Image rendering; Corresponds to View in Android.
- Element’s bridge between the two holds internal references to widgets and RenerObjects so that they can be accessed and modified quickly, including parent and child nodes. At the same time, StatefulElement can hold state and can be used for state management.
To better understand the relationship between them, let’s take an example: UI rendering is like building a building. Widgets represent drawings that show what kind of building we want to build. RenderObject is the worker who works according to the drawings, and Element is the supervisor who coordinates the resources of all parties.
RenderObject has a commonly used subclass called RenderBox, which internally creates a 2D coordinate system for measurement and layout based on the Cartesian coordinate system. If you want to customize widgets, you usually need to inherit this and implement measurement, layout, and drawing processes.
RenderObject and Element creation are expensive, so frequent creation and destruction need to be avoided.
The Element class diagram below shows the one-to-one mapping between Elemnet and Widget.
All three create diagrams
- Widget creates an Element object by calling its createElement method.
- Element went on to create its child Widget objects by calling its build method, which had a Widget object (Stateless) or State object (Stateful). The loop continues, creating child elements that hold references to the parent Element, and so eventually form an Element tree.
- RenderObjectElement is created for a layout/ Paint capable control, and its corresponding RenderObject is created in the Mount phase of that Element.
Structure diagram of the three
Let’s use the following figure to further illustrate the relationship among the three
- Widget Tree: Widgets are the upper layer interface of Flutter for developers. By nesting layers of widgets, a Widget Tree is formed. One Widget can be reused in multiple locations. The Flutter Framework layer provides some commonly used widgets for packaging or containers, such as Container, which continue to nest other widgets, such as Padding, Align, and so on. As a result, the Widget tree written by the developer is slightly different from the one actually generated. ColorBox and RawImage marked with dotted lines and circles in the figure.
- Element Tree: Each Widget corresponds to an Element, but it is classified differently.
- RenderObject Tree: The RenderObject is only responsible for the final measurement, layout, and drawing, so the final RenderObject Tree is a Tree organized by removing the packaging from the Element Tree.
What is Layer Tree?
There is also a Layer Tree concept that needs to be noted here. Let’s look at a classic flow chart of Flutter drawing.
I haven’t seen any of the three trees mentioned above, but there is a new concept of Layer Tree…
It is mainly divided into UI thread and GPU thread:
-
UI thread: The user written widget Tree is transformed into a Layer Tree. This process is implemented on the Layer of the Flutter Framework, which is also the focus of this article on UI drawing updates.
-
GPU thread: Layer Tree is synthesized by synthesizer and flattened, Skia is converted into 2D graphics instructions, and GPU instructions are rasterized by OpenGL or Vulkan hardware acceleration engine, and finally submitted to GPU for rendering. This part is implemented in the Flutter Engine layer. Although called a GPU thread, it still runs in a CPU thread.
When we were debugging the FPS Performance of the page, the Performance Overlay float opened to show how long both of them took in a single frame.
So, what is this Layer anyway?
Layer: Captures a set of RenderObject drawing results, provides translucency, displacement, clipping and other effects, and creates the final image by stacking multiple layers. In general, the Layer in the background does not need to participate in redrawing, only in composition.
The Layer Tree composed of layers will be submitted to the Flutter Engine to draw the image.
The widgets wrapped by the RepaintBoundary will eventually form a separate Layer Tree. During the marking rendering process, The RepaintBoundary interrupts the paint marking process of the parent RenderObject (normally the subwidget paint marking will also mark its parent Widget dirty, i.e. passing it up until the RepaintBoundary is reached).
Therefore, it can be used as a rendering optimization method if the child Widget does not affect the rendering of the parent Widget. In addition, when Navigator is used for routing jump, a Layer of RepaintBoundary is also packaged internally, thus forming an independent Layer between pages.
Why so many trees?
There are two reasons:
- Layering: The Framework separates the complex internal design and rendering logic from the development interface, and the application layer only focuses on Widget development.
- Efficient: The most common feature of trees is caching, because the cost of destroying and rebuilding elements and RenderObject is very high. Once reusable, caching can greatly reduce this cost. For example, when an Element does not need to be rebuilt, update the Widget’s reference. The Layer Tree is designed to separate drawing layers to facilitate extraction and composition. The transform and opacity effects in the composite Layer are only geometric transformations and opacity transformations, without triggering layout and paint, which can be directly completed by the GPU.
If the information displayed by the Flutter Inspector is insufficient, we can obtain information about various trees by calling the following methods:
- DebugDumpApp () : the Widget Tree
- DebugDumpRenderTree () : RenerObject Tree
- DebugDumpLayerTree () : Layer Tree
UI update and rendering process
To recap, what is the starting point for View update rendering in Android native development? — VSync signal. When a view’s attributes change, we use requestLayout as an example. The internal view marks itself as dirty and calls parent requestLayout level by level. Finally, the top ViewRoot registers a vsync signal with the system. The next VSync will call the ViewRootImpl’s performMeasure, performLayout, and performDraw to redraw the view.
The update process in Flutter is similar:
We focus on the Build, Layout, and Paint processes.
UI updates are triggered by calling the setState method of the State object.
What happened to setState?
- The callback function is passed in, so the code written inside the callback function is executed before drawing, often making data changes.
- Call Element’s markNeedsBuild method, internally marking itself dirty, and then call the scheduleBuildFor method of the BuildOwner object.
- BuildOwner is a Widget management class that internally records the current set of dirty Element elements. ScheduleBuildFor adds this Element to the set internally and then internally via WidgetsBinding :: The _handleBuildScheduled method continues to call Engine:: scheduleFrame indirectly, eventually registering the VSync callback. The call chain is too long to expand.
WidgetsBinding is the bridge between the Flutter Framework and the Engine. The BuildOwner object is created at initialization, and the WidgetsBinding is created in the runApp method.
When the next VSync signal arrives, the WidgetsBinding handleBeginFrame() and handleDrawFrame() are executed to update the UI.
- HandleBeginFrame handles updates to the animation state and then performs microtasks, so the execution of custom microtasks affects rendering speed.
- HandleDrawFrame performs a frame redraw of the pipeline, namely Build -> Layout -> Paint.
The process is long, so let’s look at the sequence diagram:
Let’s focus on step 11, which calls BuildOwner’s buildScope method.
The core of the visible Widget update process is to call the rebuild method for all dirty elements. Before calling the rebuild method, all dirty elements are sorted by depth in the Tree, and the elements with the lowest depth are traversed first, that is, from top to bottom.
The rebuild process is also complicated. Simply put, the dirty elements are synchronized to the Widget, Element Tree, and finally to the RenderObject Tree.
Look at the rebuild flow chart:
- PerformRebuild internally generates a newWidget by calling the Build method.
- To improve rendering performance, we want to do as little work on elements as possible, and we need to compare old and new widgets.
- Prior empty, the new widget is empty, and the old widget is not empty. This means that the widget Tree subtree is missing, and the original widget has one. Therefore, remove the old Element corresponding to the old widget from the Element Tree, and the process ends. Otherwise, the new widget is not empty and the old widget is empty. You need to create the Element corresponding to the new widget and mount it to the Element Tree (inflateWidget).
- If neither is empty, the real comparison process begins.
- Use “==” to compare references. If the references are equal, the two widgets are identical. For widgets with multichild, you need to compare slot, which identifies the position of the child Widget in the parent Widget. If the update only needs to swap the locations of sibling nodes, the process ends.
- If the reference values are different, the Widget’s static method canUpdate is further called. If true is returned, the Widget can be updated directly without changing the Element. Otherwise, the new and old widgets are still considered fundamentally different, and the original Element needs to be removed from the Tree and the new Element inflate.
- Continuous loop, if encountered without the above interrupt process, will be traversed all the way to the leaf node of the subtree.
Widget. CanUpdate method
The method in the opening Widget class diagram has only just been introduced
It is essentially comparing whether the runtimeType and key of two widgets are the same.
- RuntimeType is the type, and if the types of old and new widgets change, you obviously need to recreate Element.
- Key Flutter is another core concept of Flutter. The presence of key affects the update and reuse process of widgets.
By default, widgets are created without passing in a key, so more often you just compare the types of the two. If the types are the same, the Element of the current node does not need to be rebuilt, and the child.update call is continued to update the subtree.
By this point, I think you may have lost your understanding. For example, the following figure reflects the state changes of three trees before and after rebuild, all in the figure.
The follow-up process
When the rebuild process is complete, the dirty state of the Element is cleaned up, and the status of the Widget Tree and Element Tree is up to date. All RenderObject elements that need to be updated are added to the PipelinOwner _nodesNeedingLayout collection.
PipelinOwner: Similar to BuildOwner, BuildOwner handles the dirty elements associated with the rebuild process, and PipelinOwner handles the subsequent pipelining.
FlushLayout, step 14 in the sequence diagram, iterates through the elements from top to bottom, measuring and laying them out, and finally marks the RenderObject as needing to be redrawn by calling markNeedsPaint. Finally, in flushPaint, all needPaint elements will be redrawn, the drawing command will be recorded in the Layer, the Layer Tree information will be synchronized, and it will be submitted to the GPU thread for image synthesis and rasterization. Finally, the updated picture will be displayed by the GPU. Ok? Did you get a better understanding of the rendering flow chart above?
The process here is too long to expand in detail.
Optimization means
Const decorates the constructor
This is common in source code. In the Dart syntax, a const modifier constructor created once does not create a new object the next time it executes the constructor. Instead, it returns the previously created object, such as:
Const EdgeInsets. Symmetric (vertical: 8.0)Copy the code
Note that the only way to use const is if the input parameter is all compile-time constants, not variables.
This can be optimized because the “==” reference comparison mentioned in Step 5 above can be satisfied.
Similarly, you can create widget objects as member variables instead of creating them each time in the build function.
Update status in low-level widgets
As mentioned in the rebuild process above, if the conditions of the interrupt process are not met, the process will be traversed to the leaf node of the subtree, which itself will generate some overhead. If you can determine the area of dirty elements, try to place state-changing widgets at lower levels rather than directly at the top level of Widget setState.
Using RepaintBoundary
What is this point in the Layer Tree? As mentioned, the process of essentially breaking paint up to make it dirty will eventually form a separate Layer to improve rendering efficiency.
Inheritedwidgets and data sharing
Finally, let’s look at the Widget subclass that has nothing to do with UI drawing, but is critical to state management.
Imagine if we wanted to implement a similar theme change and update the UI.
The general idea should be an observer pattern. Wherever the topic data is used, an observer needs to be registered with the topic center. When the topic data changes, the topic center in turn notifies each observer for UI updates.
There’s a problem here, how do you define data change? In fact, it’s up to the business to decide whether the data changes or not, so we need to abstract out the InheritedWidget structure here.
The core is the updateShouldNotify method, which takes in the original widget and returns a Boolean value. The business side needs to implement this method to determine whether changes need to be notified to various observers.
Next, let’s take a look at the registration and notification process using Theme as an example.
The registration process
Let’s assume that MyWidget is our business-side Widget. After the Theme. Of (context) method is used internally to get any Theme information, a series of calls will be made to the context, the Element object corresponding to MyWidget, Register into the InheritedElement member variable Map
_dependents.
Note that the parent InheritedElement can be found in step 2 because during the Element mount process, The Map
_inheritedWidgets collection saved in the parent Widget is passed to the child widgets in turn. If an InheritedElement is also added to this collection.
When we use the MaterialApp or CupertinoApp as the root node, we already have a Theme Widget wrapped inside it, so we don’t need an extra layer to register it.
The notification process
When the parent InheritedWidget changes state, we end up calling the InheritedElement’s Update method, which we use as a starting point for notification.
As you can see, the process ends up dirtying the dependent Element, and the state of the corresponding Widget will be updated on the next frame redraw. At this point, the overall registration and notification process for the InheritedWidget ends.
If you think you have something to gain, click on the subscription button to keep from getting lost. Follow-up update plan: Key, status management, routing, please pay attention.
Refer to the article
- Flutter Architectural Overview
- Flutter rendering mechanism – UI thread
- In-depth understanding of the setState update mechanism
- Flutter Widget/Element/RenderObject/Layer Trees