Widget-Element-RenderObject
The rendering process of Flutter
1.1. The Widget – Element – RenderObject relationship
1.2. What are widgets?
Official notes on Widgets:
- The Idea behind Flutter Widgets is to build your UI to use them, inspired by React.
- The Widget uses configuration and state to describe what the View should look like.
- When a Widget changes, the Widget rebuilds its description, and the framework compares the previous description to use minimal changes to move from one state to another in the rendering tree.
Own understanding:
- Widgets are description files that are constantly built as we make state changes.
- But for render objects, only minimal overhead is used to update the render interface.
1.3. What is Element?
Official description of Element:
- Element is an instance of a Widget with a detailed location in the tree.
- Widgets describe and configure what subtrees look like, while Elements actually configure specific locations in the Element tree.
1.4. RenderObject
Official description of RenderObject:
- Renders an object in the tree
- The RenderObject layer is the core of the rendering library.
Object creation process
Let’s use Padding as an example, where Padding is used to set the inside margin
2.1. The Widget
The Padding is a Widget, and inherited from SingleChildRenderObjectWidget
The inheritance relationship is as follows:
Padding -> SingleChildRenderObjectWidget -> RenderObjectWidget -> Widget
Copy the code
We used to use statelessWidgets and statefulWidgets when creating widgets. This Widget is just a build method that assembles other widgets, not a real renderable Widget (as mentioned in the previous tutorial).
In the Padding class, we can’t find any code related to rendering. This is because the Padding is only a configuration information, which will be destroyed and created frequently depending on the properties we set.
Q: Does frequent destruction and creation affect Flutter performance?
- No, the answer is in another article of mine;
- https://mp.weixin.qq.com/s/J4XoXJHJSmn8VaMoz3BZJQ
So where does the actual rendering code go?
- RenderObject
2.2. RenderObject
If we look at the Padding, there’s a very important method:
- This method is actually a class from RenderObjectWidget, where it’s an abstract method;
- Abstract methods must be subclasses, but its subclasses SingleChildRenderObjectWidget is an abstract class, so can not realize the abstract methods of the parent
- But the Padding is not an abstract class, so the corresponding abstract method must be implemented here, and its implementation is the following one
@override
RenderPadding createRenderObject(BuildContext context) {
return RenderPadding(
padding: padding,
textDirection: Directionality.of(context),
); } Copy the code
What does the above code create? RenderPadding
What’s the inheritance of RenderPadding?
RenderPadding -> RenderShiftedBox -> RenderBox -> RenderObject
Copy the code
Let’s look at the RenderPadding source code in detail:
- If the _padding passed is the same as the saved value, return;
- If not, _markNeedResolution is called, which internally calls markNeedsLayout;
- The purpose of markNeedsLayout is to indicate that performLayout needs to be rearranged when the next frame is drawn;
- If Opacity is desired, RenderOpacity calls markNeedsPaint, and RenderOpacity has a paint method;
set padding(EdgeInsetsGeometry value) {
assert(value ! =null);
assert(value.isNonNegative);
if (_padding == value)
return;
_padding = value; _markNeedResolution(); } Copy the code
Element 2.3.
Let’s consider a question:
- Many of the widgets we wrote had references in the tree structure, but the widgets were constantly destroyed and rebuilt, which meant that the tree was very unstable;
- So who is responsible for stabilizing the tree structure of the Flutter application?
- The answer is Element.
- Official description: Element is an instance of a Widget with a detailed location in the tree.
When is Element created?
Each time a Widget is created, a corresponding Element is created and inserted into the tree.
- Element holds references to widgets;
In SingleChildRenderObjectWidget, we can find the following code:
- In the Widget, an Element is created, and this (Widget) is passed when it is created;
- Element holds applications to widgets;
@override
SingleChildRenderObjectElement createElement() => SingleChildRenderObjectElement(this);
Copy the code
After creating an Element, the Framework calls the mount method to insert the Element at a specific location in the tree:
When the mount method is called, the Widget is used to create the RenderObject and the reference to the RenderObject is kept:
- _renderObject = widget.createRenderObject(this);
@override
void mount(Element parent, dynamic newSlot) {
super.mount(parent, newSlot);
_renderObject = widget.createRenderObject(this);
assert(() {
_debugUpdateRenderObjectOwner(); return true; } ()); assert(_slot == newSlot); attachRenderObject(newSlot); _dirty = false; } Copy the code
However, if you look at a Widget of a composite class like Text, it also performs a mount method, but there is no createRenderObject called in the mount method.
- We’ve found that the primary purpose of ComponentElement is to call the _firstBuild method once it’s mounted
@override
void mount(Element parent, dynamic newSlot) {
super.mount(parent, newSlot);
assert(_child == null);
assert(_active);
_firstBuild(); assert(_child ! =null); } void _firstBuild() { rebuild(); } Copy the code
If it is a StatefulWidget, a StatefulElement is created
Let’s look at the StatefulElement constructor:
- Call widget createState()
- So StatefulElement has a reference to the created State
- In turn, _state has a reference to the widget
StatefulElement(StatefulWidget widget)
: _state = widget.createState(),
. Omit code _state._widget = widget;
Copy the code
When we call build, we essentially call the build method in _state:
Widget build() => state.build(this);
Copy the code
2.4. What is the build context
In StatelessElement, we find that we pass this in, so essentially BuildContext is the current Element
Widget build() => widget.build(this);
Copy the code
Let’s take a look at the inheritance diagram:
- Element implements the BuildContext class.
abstract class Element extends DiagnosticableTree implements BuildContext
Copy the code
In StatefulElement, the build method is similar, calling state’s build method with this passed in
Widget build() => state.build(this);
Copy the code
2.5. Create a process summary
The Widget simply describes configuration information:
- This includes the createElement method to create the Element
- CreateRenderObject is also included, but it is not called by itself
Element is an object that actually holds the tree structure:
- Once created, the framework calls the mount method;
- The mount method calls the Widget’s createRenderObject;
- And Element has references to both widgets and RenderObjects;
RenderObject is the object that is actually rendered:
- One of the
markNeedsLayout
performLayout
markNeedsPaint
paint
Methods such as
Key of Widget
When we create a Widget, we always see a key parameter. What does it do?
3.1. Case requirements for key
Let’s do a case requirement for key
The basic code for the home interface:
class _HYHomePageState extends State<HYHomePage> {
List<String> names = ["aaa"."bbb"."ccc"];
@override
Widget build(BuildContext context) {
return Scaffold( appBar: AppBar( title: Text("Test Key"), ), body: ListView( children: names.map((name) { return ListItemLess(name); }).toList(), ), floatingActionButton: FloatingActionButton( child: Icon(Icons.delete), onPressed: () { setState(() { names.removeAt(0); }); } ), ); } } Copy the code
Note: We will change the returned ListItem to ListItemLess or ListItemFul later
3.2. Implementation of StatelessWidget
ListItem is implemented using a StatelessWidget:
class ListItemLess extends StatelessWidget {
final String name;
final Color randomColor = Color.fromARGB(255, Random().nextInt(256), Random().nextInt(256), Random().nextInt(256));
ListItemLess(this.name);
@override Widget build(BuildContext context) { return Container( height: 60. child: Text(name), color: randomColor, ); } } Copy the code
The effect is that every time you delete one, all the colors will see a change
- The reason for this is very simple: after deleting the widget, call setState to rebuild it, and the new StatelessWidget will be rebuilt with a new random color
3.3. StatefulWidget implementation (without keys)
We use the StatefulWidget for ListItem
class ListItemFul extends StatefulWidget {
final String name;
ListItemFul(this.name): super(a); @override
_ListItemFulState createState() => _ListItemFulState();
} class _ListItemFulState extends State<ListItemFul> { final Color randomColor = Color.fromARGB(255, Random().nextInt(256), Random().nextInt(256), Random().nextInt(256)); @override Widget build(BuildContext context) { return Container( height: 60. child: Text(widget.name), color: randomColor, ); } } Copy the code
We found a very strange phenomenon, the color did not change, but the data moved up
- This is because the Element corresponding to the Widget does not change when the first data is deleted;
- The corresponding State reference in Element has not changed;
- When the Widget is updated, the Widget uses the State in the Element that has not changed.
3.4. Implementation of StatefulWidget (Random Key)
We use a random key
ListItemFul is modified as follows:
class ListItemFul extends StatefulWidget {
final String name;
ListItemFul(this.name, {Key key}): super(key: key);
@override
_ListItemFulState createState() => _ListItemFulState();
} Copy the code
The home interface code is modified as follows:
body: ListView(
children: names.map((name) {
return ListItemFul(name, key: ValueKey(Random().nextInt(10000))); }).toList(),
),
Copy the code
This time we find that every delete will appear a random color phenomenon:
- This is because when the key is changed, Element is forced to refresh and the corresponding State is recreated
// Code in the Widget class
static bool canUpdate(Widget oldWidget, Widget newWidget) {
return oldWidget.runtimeType == newWidget.runtimeType
&& oldWidget.key == newWidget.key;
}
Copy the code
3.5. Implementation of StatefulWidget (Name as key)
This time, we use name as the key to see the result:
body: ListView(
children: names.map((name) {
return ListItemFul(name, key: ValueKey(name));
}).toList(),
),
Copy the code
Our ideal effect:
- Because this diff algorithm is performed on the key during widget updates
- After the comparison, it is found that the Element corresponding to BBB and the Element corresponding to CCC will continue to be used, so the Element corresponding to AAA will be deleted instead of the last Element
3.6. Classification of keys
The Key itself is an abstraction, but it also has a factory constructor that creates a ValueKey
Direct subclasses include LocalKey and GlobalKey
- LocalKey, which is used to compare widgets with the same parent Element and is at the heart of the DIff algorithm;
- GlobalKey, usually we use GlobalKey for the Widget or State or Element corresponding to a Widget
3.6.1. LocalKey
There are three subclasses of LocalKey
ValueKey:
- ValueKey is used when we have a specific value as the key, such as a string, number, and so on
ObjectKey:
- If two students have the same name, it is not appropriate to use name as their key
- We can create a student object and use the object as the key
UniqueKey
- If we want to ensure that the key is unique, we can use UniqueKey;
- For example, we used random numbers to ensure that the key is different, so we can use UniqueKey;
3.6.2. GlobalKey
GlobalKey allows us to access information about a Widget, including widgets or objects such as State or Element
Let’s look at the following example: I want to be able to access the contents of HYHomePage directly from HYHomePage
class HYHomePage extends StatelessWidget {
final GlobalKey<_HYHomeContentState> homeKey = GlobalKey();
@override
Widget build(BuildContext context) {
return Scaffold( appBar: AppBar( title: Text("List test"), ), body: HYHomeContent(key: homeKey), floatingActionButton: FloatingActionButton( child: Icon(Icons.data_usage), onPressed: () { print("${homeKey.currentState.value}"); print("${homeKey.currentState.widget.name}"); print("${homeKey.currentContext}"); }, ), ); } } class HYHomeContent extends StatefulWidget { final String name = "123"; HYHomeContent({Key key}): super(key: key); @override _HYHomeContentState createState() => _HYHomeContentState(); } class _HYHomeContentState extends State<HYHomeContent> { final String value = "abc"; @override Widget build(BuildContext context) { return Container(); } } Copy the code
Note: All content will be published on our official website. Later, Flutter will also update other technical articles, including TypeScript, React, Node, Uniapp, MPvue, data structures and algorithms, etc. We will also update some of our own learning experiences