Summary:

All of these can be called widgets

In developing Flutter applications, widgets are the basic unit for “describing” the Flutter UI. Widgets can do this:

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

Google designs widgets with some distinctive features:

  • Declarative UI — Compared with 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 ina Flutter are immutable, that is, their internal members are final, and the parts of a Flutter need to be Stateful widget-state.

  • Composition over Inheritance – Widget design follows the good design philosophy of composition over Inheritance, which is to combine multiple relatively single widgets to produce relatively complex widgets.

    The nature of widgets:

    In the Widget source, there is a comment like this:

This comment illustrates the nature of the Widget: the Widget used to configure Element is essentially configuration information for the UI (with some business logic attached).

We often refer to the UI hierarchy described by widgets as a “Widget Tree”, but there is virtually no “Widget Tree” compared to “Element Tree”, “RenderObject Tree”, and “Layer Tree”. For ease of description, it is not uncommon to refer to the UI hierarchy that describes the Widget combination as a “Widget Tree.”

Classification:

Widget

Widget, the base class for all widgets.

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

  • Key Key — Used as a unique identifier between siblings under the same parent node to control how the corresponding Element is treated when the Widget is updated (whether it is updated or new). If a Widget is the only child node of its Parent Widget, you do not need to set a key.

    GlobalKey is a special type of key that will be introduced with Element.

  • Element createElement() — Each Widget has a corresponding Element created by this method. CreateElement can be interpreted as a factory method in design mode, and the specific Element type is created by the Widget subclass.

  • Static bool canUpdate(Widget oldWidget, Widget newWidget) — Can you use the newWidget to modify the Element generated in the previous frame with the oldWidget? Instead of creating a new Element, the default implementation of the Widget class is to return true if the runtimeType of both widgets is equal to the key (if the key is null, it is considered equal).

    The update process described above will also be highlighted in the introduction to Element.

StatelessWidget

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

/// A widget that does not require mutable state.
///
/// A stateless widget is a widget that describes part of the user interface by
/// building a constellation of other widgets that describe the user interface
/// more concretely. The building process continues recursively until the
/// description of the user interface is fully concrete (e.g., consists
/// entirely of [RenderObjectWidget]s, which describe concrete [RenderObject]s).
Copy the code

Specifically, there are two methods:

  • StatelessElement createElement() — The Element corresponding to the StatelessElement “StatelessWidget” is StatelessElement. Normally, StatelessWidget subclasses do not need to override this method. The Element corresponding to the subclass is also StatelessElement.

    @override
    StatelessElement createElement() => StatelessElement(this);
    Copy the code
  • Widget Build (BuildContext Context) – one of the core methods in the Flutter system

    @protected
    Widget build(BuildContext context);
    Copy the code

    The UI hierarchy and style information of the composite Widget is described in the form of a “declarative UI”, which is the main “place” for Flutter application development. This method is called in three cases:

    • The Widget is added to the Widget Tree for the first time (more specifically, when its corresponding Element is added to the Element Tree, that is, when the Element is “mounted”);
    • The “Parent Widget” modifies its configuration;
    • When the Inherited Widget depends on changes.

When the “Parent Widget” or the “Inherited Widget” changes frequently, the Build method is called frequently. Therefore, it is important to improve the performance of the Build method. Flutter officials offer several suggestions:

  • * Reduce unnecessary intermediate nodes, i.e. reduce the level of UI, * e.g. : For Single Child widgets, there is no need to combine “Row”, “Column”, “Padding”, “SizedBox” and other complex widgets to achieve a layout. Perhaps through a simple “Align”, “CustomSingleChildLayout” can be achieved. Or, to achieve a complex UI effect, it is not necessary to combine multiple “containers” and “decorations”. CustomPaint may be a better choice.

  • * Use const widgets whenever possible, * provide const constructors for widgets;

    Check out my article about const Constructor.

  • Stateless Widgets could be reconfigured to be “Stateful Widgets”, when necessary, so that some of the optimization techniques that were Stateful widgets could be used, such as: Cache the common parts of the “sub trees” and use GlobalKey when changing the tree structure;

  • Try to minimize the scope. After the use of Inherited Widgets, frequent efforts were made to extract the parts that really depended on Inherited Widgets, package them into smaller independent widgets, and push the independent widgets to the leaf node of the tree as far as possible. In order to minimize the scope of impact.

StatefulWidget

Stateful – Composite widgets, but note that the StatefulWidget itself is immutable; its mutable State resides in State.

/// A widget that has mutable state.
///
/// State is information that (1) can be read synchronously when the widget is
/// built and (2) might change during the lifetime of the widget. It is the
/// responsibility of the widget implementer to ensure that the [State] is
/// promptly notified when such state changes, using [State.setState].
Copy the code

There are two specific methods:

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

    @override
    StatefulElement createElement() => StatefulElement(this);
    Copy the code
  • State createState() — Creates the corresponding State, which is called in the StatefulElement constructor. This method is simply called when a “Stateful Widget” was added to the Widget Tree.

@protected
@factory
State createState(); // ignore: no_logic_in_create_state, this is the original sin
Copy the code
StatefulElement(StatefulWidget widget)
     : _state = widget.createState(),
       super(widget) {
   _state._element = this;
   _state._widget = widget;
 }
Copy the code

The createState method was called when Stateful Element was added to the Element Tree, which was Stateful Widget. A Widget instance can correspond to multiple Element instances (i.e., the same Widget can be configured with multiple Element nodes at different locations on the Element Tree). The createState method could be called several times during the Stateful Widget life cycle. Also, it is important to note that the Element corresponding to the Widget with a GlobalKey has only one instance in the entire Element Tree.

State

Stateful Widget logic and Stateful widgets

State was used to deal with service logic and mutable State of Stateful Widgets. Because its internal State was mutable, State had a complex life cycle:

As shown in the figure above, the life cycle of State can be roughly divided into eight phases:

  • The corresponding Element “Stateful” be mounted (mount) into a tree, through StatefulElement. Constructor – > StatefulWidget. CreateState create State instance;

    From StatefulElement. The constructor _state. _element = this; Context: BuildContext get context => _element; BuildContext get context => _element; . Once the binding relationship between a State instance and an Element instance is established, it does not change during its lifetime (the Widget corresponding to an Element may change, but the corresponding State never changes), during which time an Element can move in the tree, But the relationship was Stateful, which was Stateful.

  • StatefulElement then calls state.initState during mount, and subclasses can override this method to perform initialization operations (by referring to context, widget properties).

  • Will call the State in the process of the mount. The same didChangeDependencies, this method in the State dependent objects (such as: Inherited Widget is called when the state changes. * Child classes rarely need to override this method, * unless there is an operation that is too time-consuming to do in the build, because the build method is also called when the dependency changes;

  • At this point, State initialization has been completed, and its build method may be called many times later. When the State changes, State can trigger the reconstruction of its subtree through 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 nodes at that location in the tree with new widgets. When the old and new configurations (oldWidget, newWidget) have the same “runtimeType” && “key”, the framework replaces the oldWidget with the newWidget and triggers a series of update operations (recursively in a subtree). At the same time, the state. didUpdateWidget method is called, and subclasses override this method to respond to Widget changes;

    The above three trees and the update process will be described in more detail in a future 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). The State. Deactivate method is called. Since the removed node may be inserted back into the tree at a new location, subclasses override this method to clean up information related to the node’s location (such as the State’s references to other elements).

    The reinsert operation must occur before the animation of the current frame ends

  • When the node is reinserted into the tree, the state.build method is called again;

  • For nodes that have not been re-inserted at the end of the current frame animation, the state. dispose method is executed and the State life cycle ends, after which calling the state. setState method will report an error. Subclasses override this method to free up any occupied resources.

So far, most of the core methods in State have been introduced in the above process. Let’s focus on the setState method:

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 a few things worth noting about the setState method:

  • inState.disposeCannot be called aftersetState(line 4);
  • Cannot be called in the constructor of StatesetState(Line 7);
  • setStateMethod callback function (fn) cannot be asynchronous (return value isFuture), for the simple reason that the framework needs to refresh the UI based on the new state generated by the callback function in flow design (line 14);
  • throughsetStateMethod can update the UI by calling it from within_element.markNeedsBuild()Implemented (more on this process when we introduce Element).

Two final points about State:

  • If the state. build method depends on objects whose State changes, such as ChangeNotifier, Stream, or other objects that can be subscribed to, make sure to dispose in initState, didUpdateWidget, dispose

    Etc. 3 methods have the correct operation of subscribe and unsubscribe:

    • ininitStateSubscribe;
    • If the associated “Stateful Widget” was related to a subscription, the container was set todidUpdateWidgetUnsubscribe the old subscription before executing the new subscription.
    • indisposeExecute unsubscribe 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

ParentDataWidget and the InheritedElement we’ll cover below all inherit from The ProxyWidget, and since the ProxyWidget itself has no function as an abstract base class, ParentDataWidget and InheritedElement are introduced.

/// Base class for widgets that hook [ParentData] information to children of/// [RenderObjectWidget]s.
Copy the code

As a Proxy Widget, ParentDataWidget provides ParentData information to other widgets. Although its Child Widget is not necessarily of type RenderObejctWidget, any ParentData information it provides eventually lands on the RenderObejctWidget descendant widget.

ParentData is the auxiliary positioning information used by “parent RenderObject” in layout “Child RenderObject”. The details will be described in 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

The above code comes from RenderObjectElement, and you can see that at line 6 of its attachRenderObject method, it finds the ParentDataElement from the ancestor node, If found, use the parentData information in its Widget(ParentDataWidget) to set Render Obejct. If RenderObjectElement (line 13) is found, the current RenderObject has no Parent Data. Will call to ParentDataWidget. ApplyParentData (RenderObject RenderObject), subclasses need to rewrite the method, in order to set up corresponding RenderObject. ParentData.

Take an example, normally used in conjunction with Stack, of toy (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 is RenderObject)      targetParent.markNeedsLayout();  }}
Copy the code

As you can see, positioning itself as necessary gives the corresponding RenderObject. ParentData (in this case StackParentData), Call markNeedsLayout on the “parent Render Object” (line 19) in order to relayout, after all, the layout-related information has been modified.

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

As shown above, ParentDataWidget is defined using the generic

, which means: Trace up from the current ParentDataWidget node to the formed ancestor node chain (” parent Widget chain “), There must be at least one “RenderObject Widget” on the chain of two ParentDataWidget nodes. One “RenderObject Widget” cannot accept information from more than two “ParentData Widgets”.

/// Base class for widgets that efficiently propagate information down the tree.////// To obtain the nearest instance of a particular type of inherited widget from/// a build context, use [BuildContext.dependOnInheritedWidgetOfExactType].
Copy the code

The InheritedWidget is used to pass data down the tree. Through BuildContext. DependOnInheritedWidgetOfExactType can get the latest “Inherited the Widget”, it is important to note this way to get “Inherited the Widget”, When the status of Inherited Widget changes, the Inherited Widget will rebuild.

This principle 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 Inherited Widgets directly or with specific data.

When the Inherited Widget exists as an implementation detail of another class and is itself private (not visible externally), the of methods are placed on the public class. The most typical example is the Theme, which is a StatelessWidget type, but Inherited Widget creates a “inheritedTheme” when the of method is used:

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.

The Inherited Widget we often use is 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()— The Element corresponding to Inherited Widget isInheritedElementIn generalInheritedElementSubclasses do not override this method;
  • bool updateShouldNotify(covariant InheritedWidget oldWidget)— Determining whether peopleneed to rebuild the widgets that relied on Inherited Widgets

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

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

RenderObjectWidget

The widgets that are truly relevant to rendering are the core types, and all other types of widgets that are rendered on the screen eventually revert back to that type of Widget.

  • RenderObjectElement createElement() — The Element corresponding to the “RenderObject Widget” is RenderObjectElement. RenderObjectElement is an abstract class. So subclasses need to override this method;

  • RenderObject createRenderObject(BuildContext Context) — The core method that creates the RenderObject corresponding to the Render Widget, again overriding this method in subclasses. 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 later);

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

    Above is the source code for Flex.CreaterenderObject. Get a feel for it. As you can see, RenderFlex is initialized with the Flex information (configuration).

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

  • Void updateRenderObject(BuildContext Context, CoVariant RenderObject RenderObject) Modify the corresponding Render Object. This method is called both for the first build and when the Widget needs to be updated;

    @overridevoid 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 source code for Flex.updaterenderObject is also very simple, and corresponds almost one-to-one with flex.CreaterenderObject, modifying the renderObject with the current Flex information.

  • Void didUnmountRenderObject(Covariant RenderObject RenderObject) — This method is called when the corresponding RenderObject is removed from the Render Tree.

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

summary


Now that we’ve covered the most important basic widgets, let’s summarize:

  • Widgets are essentially UI configuration information (additional business logic) and there is no real “Widget Tree” (compared to “Element Tree”, “RenderObject Tree”, and “Layer Tree”);
  • Widgets can be divided into three functional categories: Component Widget, Proxy Widget, and Renderer Widget.
  • Widgets correspond to elements one by one. Widgets provide methods to create elements (createElement, essentially a factory method);
  • Only “Renderer Widgets” participate in the final UI generation process (Layout, Paint), and only widgets of this type have the corresponding “Render Object”, which also provides the creation method (createRenderObject).

See you next time!