Everything 's a widgetCopy the code

In Flutter, we deal with widgets all the time, and we can achieve rich UI effects by combining the basic widgets that Flutter provides. But as programmers, we are not satisfied with this, we always have a curious heart, want to explore the principle of it, hope to do and know why.

So in this article, we will demystify the Widget and explore the black box behind it.

From Opcity trigger

We’ll look at the Opcity Widget to find some clues. Opacity is a basic Widget with simple code, making it easy to analyze.

class Opacity extends SingleChildRenderObjectWidget {
  const Opacity({
    Key? key,
    required this.opacity,
    this.alwaysIncludeSemantics = false,
    Widget? child,
  }) : assert(opacity ! =null && opacity >= 0.0 && opacity <= 1.0),
       assert(alwaysIncludeSemantics ! =null),
       super(key: key, child: child);
       
  final double opacity;
  final bool alwaysIncludeSemantics;

  @override
  RenderOpacity createRenderObject(BuildContext context) {
    return RenderOpacity(
      opacity: opacity,
      alwaysIncludeSemantics: alwaysIncludeSemantics,
    );
  }

  @override
  voidupdateRenderObject(BuildContext context, RenderOpacity renderObject) { renderObject .. opacity = opacity .. alwaysIncludeSemantics = alwaysIncludeSemantics; }@override
  void debugFillProperties(DiagnosticPropertiesBuilder properties) {
    super.debugFillProperties(properties);
    properties.add(DoubleProperty('opacity', opacity));
    properties.add(FlagProperty('alwaysIncludeSemantics', value: alwaysIncludeSemantics, ifTrue: 'alwaysIncludeSemantics')); }}Copy the code

Opacity accepts only one child Widget. You can wrap any Widget with Opacity and adjust its display. In addition to the child parameter, there is only one other opacity parameter, which is a floating point type and has a value between 0.0 and 1.0. This parameter is used to control opacity.

Opacity inheritance structure is as follows:

The Opacity - SingleChildRenderObjectWidget - RenderObjectWidget > widgetsCopy the code

Usually, we use a custom Widget, are StatelessWidget/StatefulWidget inheritance. Their inheritance structure is:

StatelessWidget/StatefulWidget - > widgetsCopy the code

We can easily find StatelessWidget/StatefulWidget more is a combination of other widgets, but changed the way the Widget rendering Opcity.

We can’t find any code in the Widget related to the actual drawing. The reason is that a Widget is just a piece of configuration information, so it’s not expensive to create.

Where does the Opacity rendering take place? By name we can guess that RenderObject is responsible for rendering. In the Opacity:

/// Create a renderObject
@override
RenderOpacity createRenderObject(BuildContext context) {
    return RenderOpacity(
      opacity: opacity,
      alwaysIncludeSemantics: alwaysIncludeSemantics,
    );
}

/// Update the renderObject
@override
voidupdateRenderObject(BuildContext context, RenderOpacity renderObject) { renderObject .. opacity = opacity .. alwaysIncludeSemantics = alwaysIncludeSemantics; }Copy the code

RenderOpacity

The size of the Opacity Widget is exactly the same as its child. It’s basically the same as its child in every respect except for drawing, and it prefixes the drawing child with an opacity.

class RenderOpacity extends RenderProxyBox {
  RenderOpacity({
    double opacity = 1.0.bool alwaysIncludeSemantics = false,
    RenderBox? child,
  }) : assert(opacity ! =null),
       assert(opacity >= 0.0 && opacity <= 1.0),
       assert(alwaysIncludeSemantics ! =null),
       _opacity = opacity,
       _alwaysIncludeSemantics = alwaysIncludeSemantics,
       _alpha = ui.Color.getAlphaFromOpacity(opacity),
       super(child);

  @override
  bool getalwaysNeedsCompositing => child ! =null&& (_alpha ! =0&& _alpha ! =255);

  int _alpha;

  double get opacity => _opacity;
  double _opacity;
  set opacity(double value) {
    assert(value ! =null);
    assert(value >= 0.0 && value <= 1.0);
    if (_opacity == value)
      return;
    final bool didNeedCompositing = alwaysNeedsCompositing;
    final boolwasVisible = _alpha ! =0;
    _opacity = value;
    _alpha = ui.Color.getAlphaFromOpacity(_opacity);
    if(didNeedCompositing ! = alwaysNeedsCompositing) markNeedsCompositingBitsUpdate(); markNeedsPaint();if(wasVisible ! = (_alpha ! =0) && !alwaysIncludeSemantics)
      markNeedsSemanticsUpdate();
  }
  
  bool get alwaysIncludeSemantics => _alwaysIncludeSemantics;
  bool _alwaysIncludeSemantics;
  set alwaysIncludeSemantics(bool value) {
    if (value == _alwaysIncludeSemantics)
      return;
    _alwaysIncludeSemantics = value;
    markNeedsSemanticsUpdate();
  }

  @override
  void paint(PaintingContext context, Offset offset) {
    if(child ! =null) {
      if (_alpha == 0) {
        layer = null;
        return;
      }
      if (_alpha == 255) {
        layer = null; context.paintChild(child! , offset);return;
      }
      assert(needsCompositing);
      layer = context.pushOpacity(offset, _alpha, super.paint, oldLayer: layer as OpacityLayer?);
    }
  }

  @override
  void visitChildrenForSemantics(RenderObjectVisitor visitor) {
    if(child ! =null&& (_alpha ! =0|| alwaysIncludeSemantics)) visitor(child!) ; }@override
  void debugFillProperties(DiagnosticPropertiesBuilder properties) {
    super.debugFillProperties(properties);
    properties.add(DoubleProperty('opacity', opacity));
    properties.add(FlagProperty('alwaysIncludeSemantics', value: alwaysIncludeSemantics, ifTrue: 'alwaysIncludeSemantics')); }}Copy the code

RenderOpacity inherits from RenderProxyBox, which calls markNeedsPaint() and markNeedsLayout() methods on opacity setter. This method tells the system to redraw and rearrange.

In the paint method:

context.pushOpacity(offset, _alpha, super.paint, oldLayer: layer as OpacityLayer?)
Copy the code

Context is a high-level canvas. This line of code is an implementation of opacity.

In the end, from Opacity, we can summarize as follows:

  • Opacity is not inherited from StatelessWidget or StatefulWidget, but a SingleChildRenderObjectWidget
  • The Widget holds only the configuration information that the renderer will use
  • RenderOpacity performs the actual layout/rendering work, while the core of Widget layout is the RenderObject
  • RenderOpacity overrides the paint method. Call pushOpacity() in this method to add opacity to the Widget

At this point, we know the Opacity implementation in general, but there are still a lot of questions, right?

  • SingleChildRenderObjectWidget did what?
  • On Opacity, only widgets and renderObjects are visible, while elements are also visible.
  • What is the inheritance structure of existing widgets?
  • What do RenderProxyBox, RenderBox, and RenderObject do?

So, let’s answer these questions together.

SingleChildRenderObjectWidget, RenderObjectWidget, Widget various duties

Widget

First, let’s look at the Widget class declaration:

@immutable
abstract class Widget extends DiagnosticableTree {
  const Widget({ this.key });
  final Key? key;

  @protected
  @factory
  Element createElement();

  @override
  String toStringShort() {
    final String type = objectRuntimeType(this.'Widget');
    return key == null ? type : '$type-$key';
  }

  @override
  void debugFillProperties(DiagnosticPropertiesBuilder properties){
    super.debugFillProperties(properties);
    properties.defaultDiagnosticsTreeStyle = DiagnosticsTreeStyle.dense;
  }

  @override
  @nonVirtual
  bool operator= = (Object other) => super == other;

  @override
  @nonVirtual
  int get hashCode => super.hashCode;

  static bool canUpdate(Widget oldWidget, Widget newWidget) {
    return oldWidget.runtimeType == newWidget.runtimeType
        && oldWidget.key == newWidget.key;
  }

  static int _debugConcreteSubtype(Widget widget) {
    return widget is StatefulWidget ? 1 :
           widget is StatelessWidget ? 2 :
           0; }}Copy the code

From the Widget class declaration, we can get the following information:

  • The Widget class inherits from DiagnosticableTree and provides debugging information.
  • Key: The main function is to decide whether to reuse the old widget in the next build. This condition is determined in the canUpdate() method
  • CreateElement () : As mentioned earlier, one Widget can correspond to multiple elements; When the Flutter Framework builds the UI, it calls this method first to generate the Element object of the corresponding node. This method is implicitly invoked by the Flutter Framework and will not be invoked during our development.
  • DebugFillProperties A method that overwrites a parent class, setting DiagnosticableTree’s features.
  • CanUpdate () is a static method that is mainly used to reuse old widgets when the Widget tree is rebuilt. Specifically, whether the new Widget object is used to update the configuration of the Element object corresponding to the old UI tree; The newWidget will be used to update the configuration of the Element object as long as the runtimeType and key of the oldWidget are equal; otherwise, a new Element will be created.

RenderObjectWidget

abstract class RenderObjectWidget extends Widget {
  const RenderObjectWidget({ Key? key }) : super(key: key);

  @override
  @factory
  RenderObjectElement createElement();
  
  @protected
  @factory
  RenderObject createRenderObject(BuildContext context);

  @protected
  void updateRenderObject(BuildContext context, covariant RenderObject renderObject) { }

  @protected
  void didUnmountRenderObject(covariant RenderObject renderObject) { }
}
Copy the code

RenderObjectWidget is used to configure the RenderObject. Its createElement() function returns RenderObjectElement. Implemented by its subclasses. In contrast to the other widgets mentioned above. There is a createRenderObject() method. To instantiate the RenderObject.

RenderObjectWidget is just a configuration. When the configuration changes that need to be applied to an existing RenderObject, the Flutter framework calls updateRenderObject() to set the new configuration to the corresponding RenderObject.

RenderObjectWidget has three important subclasses:

  • LeafRenderObjectWidget The Widget configuration node is at the bottom of the tree and has no children. The corresponding LeafRenderObjectElement.
  • SingleChildRenderObjectWidget, containing only one child. The corresponding SingleChildRenderObjectElement.
  • MultiChildRenderObjectWidget, have more than one child. The corresponding MultiChildRenderObjectElement.

What is the inheritance structure of existing widgets?

The following is a list of some widgets, not all of them.

Basic inheritance diagram:

LeafRenderObjectWidget inheritance diagram:

SingleChildRenderObjectWidget inheritance diagram

MutilChildRenderObjectWidget inheritance diagram

ProxyWidget inheritance diagram

As an abstract ProxyWidget, the ProxyWidget has no real purpose. This is only used when the parent and child classes need to pass information; There are mainly InheritedWidget and ParentDataWidget. InheritedWidget and ParentDataWidget involve a lot of content, which will be further studied in subsequent articles.

StatelessWidget inheritance diagram

StatefulWidget inheritance diagram

Opacity only shows widgets and renderObjects, while Element is also visible.

The answer is SingleChildRenderObjectWidget created Element.

abstract class SingleChildRenderObjectWidget extends RenderObjectWidget {
  const SingleChildRenderObjectWidget({ Key? key, this.child }) : super(key: key);

  final Widget? child;

  /// Founded SingleChildRenderObjectElement here
  @override
  SingleChildRenderObjectElement createElement() => SingleChildRenderObjectElement(this);
}
Copy the code

Widget tree, RenderObject tree, And Element tree

As you can see from the figure above, there is a one-to-one relationship between the widget tree and the Element tree nodes. Each widget has an Element, but the RenderObject tree does not. Only the widgets that need to be rendered have nodes. The Element tree acts as an intermediate layer, a big steward, with references to both widgets and RenderObjects. When the Widget changes, compare the new Widget to the Element to see if it has the same type and Key as the original Widget. If it does, there is no need to recreate the Element and RenderObject. The RenderObject can be updated with minimal overhead by simply updating some of its properties. When the engine parses the RenderObject, it can also render with minimal overhead if only the properties have changed.

What do RenderProxyBox, RenderBox, and RenderObject do?

RenderOpacity:

RenderOpacity > RenderProxyBox > RenderBox > RenderObject > AbstractNode
Copy the code

RenderObject

RenderObject is responsible for layout and rendering. All renderObjects form a Render Tree.

The RenderObject class itself implements a basic layout and rendering protocol, but does not define a child node model (how many children can a node have? One? Two? Or more?) . It also does not define coordinate systems (e.g., are child nodes positioned in Cartesian or polar coordinates?). And specific layout protocols (by width and height or by constraint and size? , or whether the parent sets the size and position of the child nodes before or after the child node layout). To this end, Flutter provides a RenderBox class, which is derived from RenderObject. The layout coordinate system uses a Cartesian coordinate system, which is consistent with the native Android and iOS coordinate systems. The top left corner of the screen is the origin, and the screen is divided into two axes, width and height. Unless we need to customize the layout model or coordinate system, RenderBox will do just fine.

RenderBox

For a closer look at RenderBox, see:

  • RenderObject and RenderBox
  • The RenderBox instruction for Flutter: A Brief analysis of its principles

Let’s review the process of our analysis:

  1. Looking at the source code for Opacity, we know an inheritance relation of Opacity, and realize that Widge is only configuration, layout and rendering are all done by RenderObject.
  2. Then we arrange the inheritance of the commonly used widgets Flutter figure, knew the LeafRenderObjectWidget, SingleChildRenderObjectWidget, MultiChildRenderObjectWidget such Widge purposes.
  3. Finally, we’ll look at some subclasses of RenderObject to get a feel for layout and rendering.

During the whole analysis process, we will have a clear understanding of the responsibilities of the three trees and the relationship between them.

To simplify the process, let’s customize a Widget together.

The custom Widget

Complete a Widget that displays OldBirds text with a circular border.

The code is as follows:

class CircleLogoWidget extends SingleChildRenderObjectWidget {
  @override
  RenderObject createRenderObject(BuildContext context) {
    returnCircleLogoRenderBox(); }}Copy the code

Will make our CircleLogoWidget inheritance in SingleChildRenderObjectWidget default implementation a createRenderObject method, will make you return a RenderObject, This object is responsible for the drawing and layout of your widgets, and we return the CircleLogoRenderBox:


class CircleLogoRenderBox extends RenderConstrainedBox {
  CircleLogoRenderBox() : super(additionalConstraints: const BoxConstraints.tightForFinite());
  /// Whether the corresponding event is the current View used to handle event distribution
  @override
  bool hitTest(BoxHitTestResult result, {Offset position}) {
    return true;
  }

  /// Handle user touch events
  @override
  void handleEvent(PointerEvent event, covariant HitTestEntry entry) {}

  /// To draw
  @override
  voidpaint(PaintingContext context, Offset offset) { Paint _paint = Paint() .. color = Colors.red .. strokeCap = StrokeCap.round .. isAntiAlias =true. style = PaintingStyle.stroke .. strokeWidth =5.0;

    TextSpan logoSpan = TextSpan(
      text: 'OldBirds',
      style: TextStyle(
        color: Colors.blue,
        fontSize: 16,),); TextPainter textPainter = TextPainter(text: logoSpan, textDirection: TextDirection.ltr); textPainter.layout(maxWidth:180);
    /// Draw text
    textPainter.paint(context.canvas, Offset(-textPainter.size.width / 2, -textPainter.size.height / 2));
    /// Draw the circle
    context.canvas.drawCircle(offset, 80, _paint); }}Copy the code

In the CircleLogoRenderBox we’re just going to do paint.

Finally we use the CircleLogoWidget in MainPage:

class MainPage extends StatelessWidget {
  final String title;
  MainPage({this.title});
  
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text(title),
      ),
      body: Center(
        child: Opacity(
          child: CircleLogoWidget(),
          opacity: 0.5,
        ),
      ),
      floatingActionButton: FloatingActionButton(
        onPressed: (){},
        tooltip: 'Increment', child: Icon(Icons.add), ), ); }}Copy the code

We use SingleChildRenderObjectWidget complete custom CircleLogoWidget then using TextPainter to draw text, call methods like drawCircle canvans to offset as the center of the circle, Draw a circle with radius 80 to achieve the desired effect.

conclusion

Based on the in-depth interpretation of Opcity source code and further in-depth source code, this paper introduces RenderObject, and then combs the relationships among Widgets, Elements and RenderObject to further understand the drawing principle of Flutter. Finally, a custom drawable Widget is implemented.

practice

After reading this article, it should be easy to answer the following questions

  • When is the build method called?
  • What is BuildContext?
  • Do Widget creation changes frequently affect performance? What are the reuse and update mechanisms?
  • What is the purpose of creating a Key in a Widget?
  • Why is it possible to retrieve widget objects directly from state?

Refer to the

  • Flutter renders widgets, Elements and RenderObjects

To read more about Flutter, you can apply to join the Flutter wechat group by following OldBirds.