This article is the first in a series of articles about the Flutter Framework, focusing on a detailed analysis of the core approach to different types of widgets.

This article is also published on my personal blog

Overview


The Flutter is an emerging cross-platform solution that has received a lot of attention from mobile developers since it was introduced by Google at I/O in 2017, especially after the first preview was released at I/O 2018. And become the hottest cross-platform solution of the day (no one!)!

This series of articles explores the core concepts and processes of the Flutter Framework step by step, including:

  • “Widgets of the Flutter Framework”
  • “BuildOwner of the Flutter Framework”
  • Elements of the Flutter Framework
  • “PaintingContext of the Flutter Framework”
  • “Layer of the Flutter Framework”
  • PipelineOwner of the Flutter Framework
  • RenderObejct of the Flutter Framework
  • “Binding of the Flutter Framework”
  • “The Rendering Pipeline of the Flutter Framework”
  • “Custom Widgets from the Flutter Framework”

Among them, the first seven articles are basic articles, which respectively introduce several core concepts in Flutter. The Rendering Pipeline analyzes how the UI was created and updated by threading the Build, Layout and Paint processes. Finally, custom Widgets are part of the review and Practice section, which analyzes the minimum steps required to customize a Render Widget.

Such asThe figure belowAs shown, the Flutter is divided into three layers: The Framework (DART), Engine (C/C++), and Embedder (Platform). The article above focuses on the Framework layer.

Widget


Everything ‘s a widget.

In the process of developing the Flutter application, one of the most touched by widgets is undoubtedly the basic unit that “describes” the Flutter UI. Widgets can do the following:

  • Describe the hierarchy of the UI (byWidgetNested);
  • Customize the specific style of the UI (e.g.font,colorEtc.);
  • Guide the UI layout process (e.g.padding,centerEtc.);
  • .

When Google designed widgets, it also gave them some distinctive features:

  • Declarative UI — Compared with the imperative UI in traditional Native development, declarative UI has many advantages, such as significantly improved development efficiency, significantly enhanced UI maintainability, etc.

  • Immutability — All widgets in the Flutter are immutable, that is, their internal members are final, and the parts that change should be implemented “Stateful Widget-state”.

  • Composition is greater than inheritance – Widget design follows the excellent design concept of composition is greater than inheritance. By combining multiple widgets with relatively simple functions, you can get a relatively complex Widget.

inWidgetThe class definition has a comment like this:This comment makes clearWidgetThe essence of:Used to configureElementThe,WidgetThis is essentially configuration information for the UI (along with some business logic).

We often refer to the UI hierarchy described by widgets as a “Widget Tree,” but compared to “Element Tree,” “RenderObject Tree,” and “Layer Tree,” there is virtually no “Widget Tree.” It’s ok to call the UI hierarchy described by the Widget combination a “Widget Tree” for simplicity of description.

classification

As shown in the figure above, divided by functionWidgetIt can be roughly divided into three categories:

  • “Component Widget” — component-based widgets that directly or indirectly inherit from StatelessWidget or StatefulWidget. As mentioned in the previous section, composition is greater than inheritance in Widget design. You can combine widgets with more complex features than single widgets. Normal business development is focused on developing widgets of this type;

  • Proxy Widget — As the name suggests, a Proxy Widget itself does not involve the internal Widget logic, but provides additional intermediate functionality for a Child Widget. InheritedWidget is used to pass share information between “Descendant Widgets” and ParentDataWidget is used to configure the layout information in the “Descendant Renderer Widget”.

  • Renderer Widget is the most important type of Widget. It is directly involved in the Layout and Paint process. Either the “Component Widget” or the “Proxy Widget” will eventually map to the “Renderer Widget”, otherwise it will not be drawn to the screen. Of the three widgets, only the “Renderer Widget” has a corresponding “Render Object”.

Core method source code analysis

Next, let’s focus on the core methods of each type of Widget to better understand how widgets participate in the overall UI building process.

Widget

Widget, the base class for all widgets.

As shown in the figure above, there are three important methods (properties) in the Widget base class:

  • Key key— Used as a unique identifier between sibling nodes under the same parent node to control what happens to the corresponding Element when the Widget is updated (update or new). If a Widget is the only child node of its Parent Widget, the key is not required.

Globalkeys are a special class of keys that are introduced with the introduction to Element.

  • Element createElement() – Each Widget has a corresponding Element, which is created by this method. CreateElement can be understood as a factory method in the design pattern, and the specific Element type is created by the corresponding Widget subclass;

  • Static bool canUpdate(Widget oldWidget, Widget newWidget) — can you modify the Element generated by the oldWidget in the previous frame with the newWidget? Instead of creating a new Element, the default implementation of the Widget class is to return true if the runtimeType of both widgets and the key are equal, which means they can be updated directly (if the key is null, they are equal).

The above update process is also highlighted in the introduction to Element.

StatelessWidget

Stateless – composite Widget whose build method describes the hierarchy of the composite UI. The state is immutable during its life cycle.

Ps: For classes with a parent relationship, only new or modified methods are introduced in subclasses

  • StatelessElement createElement() — The Element for a StatelessWidget is a StatelessElement. StatelessWidget subclasses do not have to override this method. The Element corresponding to the subclass is also a StatelessElement;

  • The Widget build(BuildContext Context), which is one of the core methods of Flutter, describes the UI hierarchy and style information of the composite Widget in the form of a “declarative UI”. It is also the main work place for developing the Flutter application. This method is called in three situations:

    • The first time a Widget is added to the Widget Tree (more precisely, when its corresponding Element is added to the Element Tree, i.e. when the Element is “mounted”);
    • The Parent Widget has changed its configuration.
    • When the Inherited Widget that this Widget depends on changes.

When the Parent Widget or dependent Inherited Widget changes frequently, the build method is called frequently. Therefore, it is important to improve the performance of the build method. The official Flutter has several suggestions:

  • Reduce unnecessary intermediate nodes, i.e. reduce the hierarchy of the UI, such as: For a Single Child Widget, there is no need to combine complex widgets like Row, Column, Padding, and SizedBox to achieve a certain layout goal. Perhaps through a simple “Align”, “CustomSingleChildLayout” can be achieved. Or, in order to achieve a complex UI effect, it is not necessary to combine multiple containers and add “Decoration”. CustomPaint may be a better choice.

  • Use a const Widget whenever possible. Provide a const constructor for the Widget.

    Check out the comments for const Constructor recommendation Dart Constant Constructors.

  • If necessary, you can change the Stateless Widget to be Stateful Widget so that some specific optimizations in the Stateful Widget can be used, as follows: Cache the common parts of “sub trees” and use GlobalKey when changing the tree structure;

  • Try to reduce the scope of rebuilt, such as: A Widget uses “Inherited Widget”, resulting in frequent rebuilt, which can extract the parts that really depend on “Inherited Widget” and package into smaller independent widgets, and try to push this independent Widget to the leaf node of the tree. In order to reduce the affected area during redevelopment.

StatefulWidget

A stateful combined Widget, but note that the StatefulWidget itself is immutable. Its mutable State exists in State.

  • StatefulElement createElement() — The Element corresponding to StatefulWidget was StatefulElement. Generally, StatefulWidget subclasses do not need to override this method. The Element corresponding to the subclass is also a StatefulElement;

  • State createState() — Creates the corresponding State, which is called in the StatefulElement constructor. This method was called when the “Stateful Widget” was added to the Widget Tree.

// The code has been simplified (the rest of the code in this article will do the same simplification)
StatefulElement(StatefulWidget widget)
    : _state = widget.createState(), super(widget) {
    _state._element = this;
    _state._widget = widget;
}
Copy the code

In fact, when the Stateful Element corresponding to the Stateful Widget was added to the Element Tree, the createState method was invoked when the Stateful Element was initialized. A Widget instance can be configured with multiple Element instances in different locations on the Element Tree. The createState method may be invoked multiple times during the lifecycle of the Stateful Widget.

Also, note that the Element corresponding to a Widget with a GlobalKey has only one instance in the entire Element Tree.

State

The logic and internal state were a Stateful Widget.

State is used to process the business logic and mutable State of the Stateful Widget. Because its internal State is variable, State has a more complex life cycle:As shown in the figure above, the life cycle of State can be roughly divided into 8 phases:

  • Passed when the corresponding Stateful Element was mounted to the treeStatefulElement.constructor –> StatefulWidget.createStateCreate a State instance;

From StatefulElement. The constructor _state. _element = this; We know that state. _emelent refers to the Element instance that we know state. context refers to: BuildContext get context => _element; .

Once a State instance is bound to an Element instance, it does not change throughout its lifetime (the Element’s Widget may change, but its State never does), while the Element can move around the tree. But the relationship above does not change (that is, “Stateful Element” moves state).

  • StatefulElement then calls state.initState during the mounting process. Subclasses can override this method to perform the initialization (by referring to the Context or widget properties).

  • Will call the State in the process of the mount. The same didChangeDependencies, this method in the State dependent objects (such as: * Subclasses rarely need to override this method, * unless there are time-consuming operations that are not appropriate to do in a build, because Inherited Widgets are called when dependencies change;

  • At this point, the initialization of State is complete, and its build method may be called several times. When the State changes, State can trigger the rebuilding of its subtree through the setState method.

  • At this point, the “Element Tree”, “RenderObject Tree”, and “Layer Tree” have been built and the complete UI should be rendered. Since then, the parent Element in the “Element Tree” may rebuild the node in the tree with the new configuration (Widget). When the old and new configurations (oldWidget, newWidget) have the same “runtimeType” && “key,” the framework replaces the oldWidget with the newWidget, triggering a series of updates (recursively in the subtree). At the same time, the state.didupDateWidget method is called, and the subclass overrides the method to respond to the Widget changes;

The above three trees and the update process will be covered in more detail in a subsequent article

  • In the process of UI update, any node may have been removed, the State will be subsequently removed, (as in step “runtimeType” | | “key” is not equal to). And then it callsState.deactivateMethod, since the removed node may be reinserted at a new location in the tree, subclasses override this method to clean up information related to the node’s location (such as the State’s reference to other elements). No resource cleanup should be done in this method.

The reinsert operation must precede the end of the current frame animation

  • The state.build method is called again when the node is reinserted into the tree;

  • For nodes that have not been reinserted at the end of the current frame animation, the state-. Dispose method is executed, ending the State lifecycle, and an error will be reported if the state-.setState method is called thereafter. Subclasses override this method to free any occupied resources.

So far, most of the core methods in State have been introduced in the above process, so let’s focus on themsetStateMethods:

void setState(VoidCallback fn) {
  assert(fn ! =null);
  assert(() {
    if (_debugLifecycleState == _StateLifecycle.defunct) {
      throwFlutterError.fromParts(<DiagnosticsNode>[...] ); }if(_debugLifecycleState == _StateLifecycle.created && ! mounted) {throwFlutterError.fromParts(<DiagnosticsNode>[...] ); }return true; } ());final dynamic result = fn() as dynamic;
  assert(() {
    if (result is Future) {
      throwFlutterError.fromParts(<DiagnosticsNode>[...] ); }return true; } ()); _element.markNeedsBuild(); }Copy the code

As you can see from the above source code, there are several things to note about the setState method:

  • SetState cannot be called after state.Dispose;

  • You cannot call setState in the State constructor;

  • The setState callback (FN) function cannot be asynchronous (return Future) simply because the process design requires the framework to refresh the UI based on the new state generated by the callback function.

  • The ability to update the UI with the setState method is implemented internally by calling _element.markNeedsbuild () (more on this when we introduce Element).

Two final points about State:

  • If the state. build method depends on an object whose State changes, such as: ChangeNotifier, Stream, or any other object that can be subscribed to, Make sure there are proper subscribe and unsubscribe operations between initState, didUpdateWidget, Dispose method and so on:

    • ininitStateSubscribe in;
    • If the associated Stateful Widget was related to a subscription, the interface was displayed indidUpdateWidgetCancel the old subscription before executing the new subscription.
    • indisposeUnsubscribe in.
  • In the State. InitState method cannot call BuildContext. DependOnInheritedWidgetOfExactType, but State. With the execution of didChangeDependencies and can be invoked in this method.

ParentDataWidget

ParentDataWidgetAnd the followingInheritedElementAll inherit fromProxyWidgetBecause ofProxyWidgetAs an abstract base class, it has no function of its own, so it is introduced directly belowParentDataWidget,InheritedElement.ParentDataWidgetAs a Proxy Widget, its functions are mainly provided for other widgetsParentDataInformation. Although its Child Widget is not necessarily of type RenderObejctWidget, it is providedParentDataThe information eventually lands on the RenderObejctWidget type descendant Widget.

ParentData is the auxiliary positioning information used by the parent RenderObject and the Layout Child RenderObject. Details are described in the section about RenderObject.

void attachRenderObject(dynamic newSlot) {
  assert(_ancestorRenderObjectElement == null); _slot = newSlot; _ancestorRenderObjectElement = _findAncestorRenderObjectElement(); _ancestorRenderObjectElement? .insertChildRenderObject(renderObject, newSlot);final ParentDataElement<RenderObjectWidget> parentDataElement = _findAncestorParentDataElement();
  if(parentDataElement ! =null)
    _updateParentData(parentDataElement.widget);
}

ParentDataElement<RenderObjectWidget> _findAncestorParentDataElement() {
  Element ancestor = _parent;
  while(ancestor ! =null && ancestor is! RenderObjectElement) {
    if (ancestor is ParentDataElement<RenderObjectWidget>)
      return ancestor;
    ancestor = ancestor._parent;
  }
  return null;
}

void _updateParentData(ParentDataWidget<RenderObjectWidget> parentData) {
  parentData.applyParentData(renderObject);
}
Copy the code

So the code above is from RenderObjectElement, and you can see that at line 6 of its attachRenderObject method we take the ParentDataElement from the ancestor node, If found, set Render Obejct with the parentData information in its Widget(ParentDataWidget). If RenderObjectElement (line 13) is found during lookup, the RenderObject does not have Parent Data. Will call to ParentDataWidget. ApplyParentData (RenderObject RenderObject), subclasses need to rewrite the method, in order to set up corresponding RenderObject. ParentData.

Look at an example of this, usually paired with a Stack of tourists (inherited from ParentDataWidget) :

void applyParentData(RenderObject renderObject) {
  assert(renderObject.parentData is StackParentData);
  final StackParentData parentData = renderObject.parentData;
  bool needsLayout = false;

  if(parentData.left ! = left) { parentData.left = left; needsLayout =true; }...if(parentData.width ! = width) { parentData.width = width; needsLayout =true; }...if (needsLayout) {
    final AbstractNode targetParent = renderObject.parent;
    if (targetParent isRenderObject) targetParent.markNeedsLayout(); }}Copy the code

As can be seen, c-bound assigns its property to the corresponding RenderObject. ParentData (here StackParentData) when necessary, Call markNeedsLayout(line 19) on the “parent Render object” to rearrange the layout, after all the layout information has been changed.

abstract class ParentDataWidget<T extends RenderObjectWidget> extends ProxyWidget
Copy the code

As shown above, ParentDataWidget is defined using the generic

. The meaning behind it is: Tracing up the parent Widget chain from the current parent Datawidget node, There must be at least one “RenderObject Widget” node in the chain of two ParentDataWidget nodes. Because a “RenderObject Widget” cannot accept information from two or more “ParentData Widgets”.

InheritedWidget

The InheritedWidget is used to pass data down the tree. throughBuildContext.dependOnInheritedWidgetOfExactTypeYou can retrieve the most recent Inherited Widget. Note that when this method is used, when the status of the Inherited Widget changes, the reference will be rebuilt.

The principles will be analyzed in detail in the introduction to Element.

Often, in order to use convenient “Inherited the Widget” will provide of static method, this method invokes the BuildContext. DependOnInheritedWidgetOfExactType. The “of” method can return to Inherited Widgets directly, as well as specific data.

Sometimes, Inherited Widgets exist as implementation details for another class, which is itself private (not externally visible), and the of method is placed on the public class. A typical example is a Theme, which is a StatelessWidget type, but creates a Inherited Widget: _InheritedTheme. The of method is defined on the previous Theme:

static ThemeData of(BuildContext context, { bool shadowThemeOnly = false{})final _InheritedTheme inheritedTheme = context.dependOnInheritedWidgetOfExactType<_InheritedTheme>();

  return ThemeData.localize(theme, theme.typography.geometryThemeFor(category));
}
Copy the code

The of method returns the ThemeData specific data types, and within the first calls the BuildContext. DependOnInheritedWidgetOfExactType.

Inherited Widgets are often used with MediaQuery, which also provides the “of” method:

static MediaQueryData of(BuildContext context, { bool nullOk = false{})final MediaQuery query = context.dependOnInheritedWidgetOfExactType<MediaQuery>();
  if(query ! =null)
    return query.data;
  if (nullOk)
    return null;
}
Copy the code

  • InheritedElement createElement method () – “Inherited the Widget” corresponding Element for InheritedElement, usually InheritedElement subclasses don’t have to rewrite the method;

  • Bool updateShouldNotify(Covariant InheritedWidget oldWidget) – Determine if we need to rebuild those that depend on it when “InheritedWidget” underway The Widget.

Below is MediaQuery updateShouldNotify implementation, in the old and new Widget. The data is not equal to rebuilt the dependence of the Widget.

boolupdateShouldNotify(MediaQuery oldWidget) => data ! = oldWidget.data;Copy the code

RenderObjectWidget

Widgets that are truly rendering-related are the core type, and all other types of widgets that are rendered to the screen will eventually return to this type of Widget.

  • RenderObjectElement createElement() — The Element in the “RenderObject Widget” is RenderObjectElement, and RenderObjectElement is abstract, So subclasses need to override this method;

  • RenderObject createRenderObject(BuildContext Context) — The core method that creates the RenderObject corresponding to the Render Widget. Again, subclasses need to override this method. This method is called when the corresponding Element is mounted to the Tree (element.mount), so the “Render Tree” is built synchronously while the Element is mounted (more on this in a later article).

@override
RenderFlex createRenderObject(BuildContext context) {
  return RenderFlex(
    direction: direction,
    mainAxisAlignment: mainAxisAlignment,
    mainAxisSize: mainAxisSize,
    crossAxisAlignment: crossAxisAlignment,
    textDirection: getEffectiveTextDirection(context),
    verticalDirection: verticalDirection,
    textBaseline: textBaseline,
  );
}
Copy the code

The above is the source code of Flex. CreateRenderObject, really feel (or code more feel). As you can see, RenderFlex is initialized with Flex’s information (configuration).

Flex is the base class for Row and Column. RenderFlex inherits from RenderBox, which continues from RenderObject.

  • void updateRenderObject(BuildContext context, covariant RenderObject renderObject)— core method that modifies the corresponding Render Object after the Widget is updated. This method is called on the first build and when the Widget needs to be updated;
@override
void updateRenderObject(BuildContext context, covariantRenderFlex renderObject) { renderObject .. direction = direction .. mainAxisAlignment = mainAxisAlignment .. mainAxisSize = mainAxisSize .. crossAxisAlignment = crossAxisAlignment .. textDirection = getEffectiveTextDirection(context) .. verticalDirection = verticalDirection .. textBaseline = textBaseline; }Copy the code

The flex.updaterenderObject source code is also very simple, almost one by one, corresponding to the Flex.createrenderObject, modifying the renderObject with the current Flex information.

  • void didUnmountRenderObject(covariant RenderObject renderObject)Call this method when the corresponding Render Object is removed from the Render Tree.

RenderObjectWidget subclasses LeafRenderObjectWidget, SingleChildRenderObjectWidget, MultiChildRenderObjectWidget just rewrite the createElement method method to return their corresponding concrete Instance of the Element class.

summary


Now that we’ve covered the basics of important widgets, let’s summarize:

  • Widgets are essentially UI configuration information (additional business logic), there is no real “Widget Tree” (as opposed to “Element Tree”, “RenderObject Tree”, and “Layer Tree”);

  • Widgets fall into three functional categories: “Component Widget”, “Proxy Widget”, and “Renderer Widget”.

  • The Widget corresponds to one Element, and the Widget provides a method to create an Element (createElement, essentially a factory method).

  • Only the “Renderer Widget” takes part in the final UI generation process (Layout, Paint), and only this type of Widget has a corresponding “RenderObject” that also provides the creation method (createRenderObject).

See you next time!