Everything in Flutter is made up of widgets, including buttons, text, images, lists, layouts, gestures, animations, etc. Developers build their UI by combining and nesting widgets.
This article will explore the design ideas behind Flutter Widgets and delve into the source code to understand how they are implemented so that we can better develop the UI using the widgets.
Design idea
Flutter takes inspiration from React and creates beautiful components with a modern framework. The core idea is to build your UI with widgets. Widgets describe what the view should look like in its current configuration and state. When the widget’s state changes, it reconstructs the UI it is presenting, and the framework compares the changes to determine the minimum changes needed to transition the underlying rendering tree from one state to the next.
React’s core declarative and component-based programming has been inherited by Flutter. Widgets use both declarative and component-based programming paradigms.
A programming paradigm is a programming style that provides (and determines) a programmer’s view of how a program executes. For example, in object-oriented programming, programmers think of a program as a series of interacting objects, whereas in functional programming a program is thought of as a sequence of stateless functions evaluated.
Declarative programming
Declarative is a programming paradigm that describes what the UI looks like, rather than directly instructing how the UI is built step by step. Declarative programming is often contrasted with imperative programming, which requires algorithms to specify exactly what to do at each step.
For declarative programming, instead of writing code to manipulate view commands when rendering an interface, we modify the data and let the framework transform the data into the view. Data is the UI data model of components. Developers design a reasonable data model according to needs, and the framework renders the UI interface according to data. This approach lets developers only manage and maintain data state, greatly reducing the burden on developers.
The use of UITableView in iOS development is similar to declarative programming, so let’s make a comparison to help understand declarative programming. Usually, the dataSource dataSource is prepared first, and then the items in the dataSource are mapped into cells. When the dataSource dataSource changes, the UITableView will refresh accordingly.
Let’s take another example to illustrate the difference between imperative and declarative programming
In imperative programming, it is common to use the selector findViewById or similar function to get instance B of ViewB and call the relevant method to use it, as follows
// Imperative style b.setColor(red) b.clearChildren() ViewC c3 = new ViewC(...) b.add(c3)Copy the code
In declarative programming, when the UI needs to change, we call setState() on the StatefulWidgets component to change the data and rebuild the Widget tree.
Declarative networkreturn ViewB(
color: red,
child: ViewC(...),
)
Copy the code
The problem with React/Flutter is that any state change in the view will re-render the whole view, causing an unnecessary refresh.
React uses Component to describe the interface, while Flutter uses widgets to describe the interface. Components and widgets are “configuration information” for view content, not elements that are actually rendered on the screen. The creation and destruction of these configuration objects does not incur significant performance costs. However, the reconstruction of objects that are actually responsible for rendering is very expensive and will not be easily reconstructed.
When the data state changes, the framework recalculates to generate a new component tree, and compares the differences between the new and old component trees to find the changed components for re-rendering. This renders only the components that have changed and does not refresh the components that have not changed, avoiding the unnecessary performance cost of the overall refresh.
componentization
In the React/Flutter world, everything is a component. A component is a configuration or description of a UI element that describes what is displayed on the screen. It can be said that the user interface is a component.
Component, can be regarded as a state machine, through the interaction with the user, change different states, when the component is in a certain state output corresponding UI, when the component state changes, according to the new state rendering view, data and view are always consistent.
A component is a relatively independent entity that contains logic/style/layout, and even static resources that depend on it. The related code is encapsulated in a single unit to avoid conflicts with other code as much as possible. This design allows for high cohesion and low coupling, with components performing their own functions as independently as possible, independent of external code.
Simple components are nested and combined to form large components, and then a complex interface is constructed. In this way, the interface is easy to understand and maintain.
Consider: Is the component in the React/Flutter MVVM
Realize the principle of
The function of a Widget in Flutter is to configure or describe a UI element, not an element that is actually displayed on the device. The class that really represents the elements displayed on the device is Element, and the class that really does the layout and drawing is RenderObject.
Let’s start with a few questions to clarify how widgets work. 1. What are widgets, Elements, and RenderObjects? 2. What are the relationships among widgets, Elements, and RenderObjects? 3. How are widgets, Elements, and RenderObjects generated and related? 4. How will the view be re-rendered when widgets are updated in the page? 5. How is the Element tree updated?
1. What are widgets, Elements, and RenderObject, and what functions are each responsible for?
A Widget is a configuration or description of a UI element that holds rendered content, layout information, and so on.
For widgets, it is immutable and cannot be modified once created. When the user interface changes, Flutter does not modify the old Widget tree, but creates a new Widget tree. Because the Widget is lightweight and only a “blueprint” that does not involve actual view rendering, frequent destruction and rebuilding do not pose performance problems.
Elements are generated from widgets and are instantiated objects of widgets that have a one-to-one correspondence. Element holds both widgets and RenderObjects and acts as a bridge between configuration information and the final render.
Once an Element is created, it is inserted into the UI tree. If the Widget changes later, it is compared to the old Widget and the corresponding Element is updated.
Due to the immutability of widgets, when the Widget tree is rebuilt, the Element tree compares the old and new Widget trees, finds the changed nodes, and synchronizes them to the RenderObject tree. Finally, only the changed nodes are rendered, improving rendering efficiency.
RenderObject is responsible for layout and drawing.
2. What are the relationships among widgets, Elements, and RenderObjects?
There are three trees in Flutter’s UI system: the Widget tree, the Element tree, and the render tree. Their relationship is that the Element tree is generated from the Widget tree, and the render tree is generated from the Element.
When the Widget tree changes, the corresponding Element tree is rebuilt and the render tree is updated.
3. How are widgets, Elements, and RenderObjects generated and related?
The entry to the Flutter program is the void runApp(Widget App) method, which is called when the app starts. This method passes in the first rootWidget to be displayed, then creates the rootElement and associates the rootWidget with the rootElement. Widget property. After rootElement is created, call its own mount method to create a rootRenderObject and associate the rootRenderObject with the rootElement. RenderObject property.
After rootElement is created, the buildScope method is called to create the Child Widget tree. The widget corresponds to an element. The Child Widget calls the createElement method to create a Child element with itself as an argument. The child Element then mounts itself to the rootElement. Form a tree.
At the same time call widget. CreateRenderObject create child renderObject, and mounted on rootRenderObject.
RootElement and renderView (RenderObject subclasses) are global singletons that are created only once.
4. How will the view be re-rendered when widgets are updated in the page?
StatefullWidget, a subclass of Widget, can create a corresponding State object that triggers a view refresh by calling the state.setState() method.
The state.setState() method calls markNeedsBuild internally, and the Element that marks the StatefullWidget needs to be refreshed
_element.markNeedsBuild();
Copy the code
When the drawFrame of the next cycle is drawn, performRebuild() is re-called, triggering an Update to the Element tree, updating itself and the associated RenderObject with the latest Widget tree, and then the row layout and drawing process begins.
5. How is the Element tree updated?
When a StatefullWidget changes, find the corresponding Element node and set its dirty to True. The next time a frame draws a drawFrame, re-call performRebuild() to update the UI
newWidget == null | newWidget ! = null | |
---|---|---|
child == null | Returns null. | Returns new [Element]. |
child ! = null | Old child is removed, returns null. | Old child updated if possible, returns child or new [Element]. |
As shown above, there are four cases when the new widget is compared to the widget inside the old child,
The new widget is empty, the old widget is empty, and null is returned
If the new widget is null and the old widget is not, the old child is removed and null is returned
The new widget is not empty, the old widget is empty, create a new Element and call mount to embed it in the tree
The new widget is not empty, and the old widget is not empty. Check whether the old child can be updated, and update the child if it is. If not, remove the old child, create a new Element and return it
Source code analysis
Let’s start by analyzing the Widget source code with a Demo of Hellow World.
The entry point to the program is the runApp method, which passes in the interface Widget to display.
void main() {
runApp(
Center(
child: Text(
'Hello, world! ',
textDirection: TextDirection.ltr,
),
),
);
}
Copy the code
Enter the runApp method, where WidgetsFlutterBinding is a bridge class that connects to the underlying Flutter Engine SDK to receive and process messages from the Flutter engine. The Flutter Engine is responsible for layout, drawing, platform messaging, gestures, and more.
WidgetsFlutterBinding inherits from BindingBase. BindingBase Mixin has seven classes: GestureBinding, ServicesBinding, SchedulerBinding, PaintingBinding, SemanticsBinding, RendererBinding, WidgetsBinding. Mixins are similar to multiple inheritance in that a method of the same name of a later class overrides a method of the previous class. These classes are grouped together to listen for messages from the Flutter engine.
WidgetsFlutterBinding is a singleton class initialized by the ensureInitialized method that returns a singleton object
void runApp(Widget app) { WidgetsFlutterBinding.ensureInitialized() .. attachRootWidget(app) .. scheduleWarmUpFrame(); }Copy the code
AttachRootWidget method is introduced to rootWidget, rootWidget and renderView packaging to RenderObjectToWidgetAdapter (RenderObject subclass). RenderView is created at initialization with the aforementioned RendererBinding, which is a subclass of RenderObject responsible for the actual layout and drawing.
RenderObjectToWidgetAdapter inherited from the Widget, rewrite the createElement method and createRenderObject method, these two methods in building Element and RenderObject behind is used, CreateElement method returns the RenderObjectToWidgetElement type object, createRenderObject returns the renderView.
RenderViewElement and renderView are attributes of the WidgetsFlutterBinding class, and WidgetsFlutterBinding is singleton mode. The natural renderViewElement and renderView are globally unique.
Element get renderViewElement => _renderViewElement; Element _renderViewElement; void attachRootWidget(Widget rootWidget) { _renderViewElement = new RenderObjectToWidgetAdapter<RenderBox>( container: RenderView, debugShortDescription: '[root]', Child: rootWidget). AttachToRenderTree (buildOwner); }Copy the code
AttachToRenderTree method creates and returns a RenderObjectToWidgetElement object, the first call to create a new RenderObjectToWidgetElement objects, called again to reuse existing.
CreateElement method method will RenderObjectToWidgetAdapter itself as a parameter, the initialization RenderObjectToWidgetElement object, So RenderObjectToWidgetElement can take to rootWidget and renderView, so that the three links.
RenderObjectToWidgetElement<T> createElement() => RenderObjectToWidgetElement<T>(this);
RenderObjectToWidgetElement<T> attachToRenderTree(BuildOwner owner, [RenderObjectToWidgetElement<T> element]) {
if(element == null) { owner.lockState(() { element = createElement(); assert(element ! = null); element.assignOwner(owner); }); owner.buildScope(element, () { element.mount(null, null); }); }else {
element._newWidget = this;
element.markNeedsBuild();
}
return element;
}
Copy the code
As an Element is created, a RenderObject is created. In the superclass RenderObjectElement mount method, called createRenderObject RenderObject, the widget is RenderObjectToWidgetAdapter here, _renderObject is RenderObjectToWidgetAdapter hold renderView object
void mount(Element parent, dynamic newSlot) { super.mount(parent, newSlot); / / / see section 2.5.5 _renderObject = widget. CreateRenderObject (this); attachRenderObject(newSlot); // Attach newSlot to RenderObject _dirty =false;
}
Copy the code
At this point, both Element and RenderObject are created. Go back to the mount RenderObjectToWidgetElement method, it calls the _rebuild method, _rebuild updateChild method is called
void mount(Element parent, dynamic newSlot) {
assert(parent == null);
super.mount(parent, newSlot);
_rebuild();
}
void _rebuild() { try { _child = updateChild(_child, widget.child, _rootChildSlot); } catch (exception, stack) { ... }}Copy the code
The new widget is null, the old widget is null, and the old widget is not null. The old child is removed. Return null the new widget is not empty, the old widget is empty, create a new Element and call mount to embed the new widget into the tree. Check whether the old child can be updated, and update the child if it can. If not, remove the old child, create a new Element and return it
Element updateChild(Element child, Widget newWidget, dynamic newSlot) {
if (newWidget == null) {
if(child ! = null) deactivateChild(child);return null;
}
if(child ! = null) {if (child.widget == newWidget) {
if(child.slot ! = newSlot) updateSlotForChild(child, newSlot);return child;
}
if (Widget.canUpdate(child.widget, newWidget)) {
if(child.slot ! = newSlot) updateSlotForChild(child, newSlot); child.update(newWidget);return child;
}
deactivateChild(child);
}
return inflateWidget(newWidget, newSlot);
}
Element inflateWidget(Widget newWidget, dynamic newSlot) {
final Key key = newWidget.key;
if (key is GlobalKey) {
final Element newChild = _retakeInactiveElement(key, newWidget);
if(newChild ! = null) { newChild._activateWithParent(this, newSlot); final Element updatedChild = updateChild(newChild, newWidget, newSlot);return updatedChild;
}
}
final Element newChild = newWidget.createElement();
newChild.mount(this, newSlot);
return newChild;
}
Copy the code
Now that the app is complete from startup to first rendering, let’s look at what happens internally when the page Widget is updated, starting with the setState() method
abstract class State<T extends StatefulWidget> extends Diagnosticable {
StatefulElement _element;
void setState(VoidCallback fn) { ... _element.markNeedsBuild(); }}Copy the code
Set element.dirty to true, marking the element to be refreshed and placing it in the dirty element array for processing during the next cycle of rendering.
OnBuildScheduled is created when the WidgetsBinding is initialized, and calls uI.window.scheduleFrame () inside the method to tell the underlying engine to refresh the frame
abstract class Element extends DiagnosticableTree implements BuildContext {
void markNeedsBuild() {
if(! _active)return;
if (dirty)
return;
_dirty = true;
owner.scheduleBuildFor(this);
}
}
void scheduleBuildFor(Element element) {
if (element._inDirtyList) {
_dirtyElementsNeedsResorting = true;
return;
}
if(! _scheduledFlushDirtyElements && onBuildScheduled ! = null) { _scheduledFlushDirtyElements =true;
onBuildScheduled();
}
_dirtyElements.add(element);
element._inDirtyList = true;
}
Copy the code
Engine notifies the page to refresh and finally calls buildScope in the drawFram method, which sorts the dirty array first, with shallow nodes first and deep nodes later, to avoid rebuilding the child nodes first and then the parent nodes again.
void drawFrame() { try { buildOwner.buildScope(renderViewElement); . buildOwner.finalizeTree(); } finally { } } void buildScope(Element context, [VoidCallback callback]) { try { ... _dirtyElements.sort(Element._sort); // Sort dirty elements... int dirtyCount = _dirtyElements.length; int index = 0; // Walk through the dirty elements and rebuildwhile (index < dirtyCount) {
try {
_dirtyElements[index].rebuild();
} catch (e, stack) {
}
index += 1;
}
} finally {
for (Element element in _dirtyElements) {
element._inDirtyList = false; } _dirtyElements.clear(); . }}Copy the code
Element’s rebuild method eventually calls performRebuild(), which in turn calls the updateChild method, as described earlier. In the updateChild method, Comparing the widgets of the old and new nodes, there are four cases where only the node that needs to be changed is updated.
The resources
Flutter
1. React design philosophy — Beauty in simplicity
Disputes of empires -FlutterUI Draw parsing
In-depth understanding of the setState update mechanism
The practice of Flutter in Ming Shi Tang