background

Why does Flutter mean everything is a Widget? First of all, you need to know what Flutter is. It is a modern responsive framework, a 2D rendering engine, ready-made widgets and development tools based on Skia, a powerful 2D rendering engine that was acquired by Google in 2005 and is widely used on Chrome and Android, etc. The Flutter is a UI framework, so everything is a Widget, and Widget means a small part in Chinese. Why can’t it be called a View like Android or Ios? Because widgets can be a structural element (such as a button or menu), a text style element (such as a font or color scheme), an aspect of a layout (such as a fill), and so on, we can organize them as Wigets rather than views, which is a reasonable naming abstraction based on basic naming conventions. So what do we learn next?

  • What is a Widget
  • The Widget class structure
  • Follow me to implement a Widget (directly inheriting the Widget abstract class)
  • Element class structure
  • Deeper understanding of Element

What is a Widget

It’s not. Because it’s based on Dart, there are a lot of Dart libraries that you can use, like AES, RSA encryption and decrypting, Json serialization, and so on. But you can say that anything related to building graphics is a Widget, and that’s what a Widget is

The Widget class structure

Why the class structure? The class structure is a very clear way to sort out the logic and look at the whole structure from a global perspective

  • RenderObjectWidget is the Widget that holds the RenderObject object, which is actually a subclass of it that does the layout, measurement, and drawing of the interface, like Padding, Table, and Align
  • StatefulWidget has a State State Widget that can be changed dynamically in subclasses such as CheckBox and Switch
  • StatelessWidget is a common Widget that is immutable like Icon or Text.
  • ProxyWidget InheritedWidget is a subclass of it, we’ll assume it is the key to subclasses can get data from the parent class, later research, most of the topics are inherited from ProxyWidget

Implement a Widget with me

I don’t want to get into the same mindset as other tutorials. Since everything is a Widget, let’s start by implementing a Widget and then work our way through it, learning as we see it. To the code

class TestWidget extends Widget{
  @override
  Element createElement() {
    // TODO: implement createElement
    throwUnimplementedError(); }}Copy the code

Create a TestWidget and then inherit the Widget, and then it will ask you to override the createElement function to return an Element, and from this we can see that the Widget that we’re creating is actually creating an Element. So what is an Element? In the same vein, let’s take a look at Element

class TestElement extends Element{

  TestElement(Widget widget) : super(widget);

  @override
  bool get debugDoingBuild => throw UnimplementedError();

  @override
  void performRebuild() {
  }

}
Copy the code

We have a constructor that passes the Widget object, we have a get function, we have a debugDoingBuild function, we have a performRebuild function, what does that do?

abstract class Element extends DiagnosticableTree implements BuildContext 

abstract class BuildContext {

  /// Whether the [widget] is currently updating the widget or render tree.
  ///
  /// For [StatefulWidget]s and [StatelessWidget]s this flag is true while
  /// their respective build methods are executing.
  /// [RenderObjectWidget]s set this to true while creating or configuring their
  /// associated [RenderObject]s.
  /// Other [Widget] types may set this to true for conceptually similar phases
  /// of their lifecycle.
  ///
  /// When this is true, it is safe for [widget] to establish a dependency to an
  /// [InheritedWidget] by calling [dependOnInheritedElement] or
  /// [dependOnInheritedWidgetOfExactType].
  ///
  /// Accessing this flag in release mode is not valid.
  bool get debugDoingBuild;
   
Copy the code

After tracing the code we found some comments:

  • Element inherits from DiagnosticableTree and implements BuildContext
  • DiagnosticableTree is a “diagnostic tree” that provides debugging information.
  • BuildContext is similar to the native system context, it defines the debugDoingBuild, which, as we know from the annotations, should be a flag bit for a debug.
  • PerformRebuild after checking the source code, it is found that the call from rebuild() is as follows
void rebuild() { if (! _active || ! _dirty) return; performRebuild(); } @override void update(ProxyWidget newWidget) { rebuild(); }Copy the code

StatelessElement is a subclass of Element. This means that after the update function, the Element directly executes performRebuild. So let’s complete the custom Element logic

class TestElement extends Element {

  TestElement(Widget widget) : super(widget);

  @override
  bool get debugDoingBuild => throw UnimplementedError();

  @override
  void performRebuild() {
  }

  @override
  void update(Widget newWidget) {
    super.update(newWidget);
    print("TestWidget update");
    performRebuild();
  }

  @override
  TestWidget get widget => super.widget as TestWidget;

  Widget build() => widget.build(this);
}
Copy the code

PerformRebuild () is executed with the update, but what does performRebuild do? Let’s look at the StatelessElement implementation and see that it calls the Widget parameter build function passed in, so let’s add the function to TestWidget and refine the logic

Class TestWidget extends Widget {@override Element createElement() { Have Element call the following build function return TestElement(this); } /// The context is the Element Widget build(BuildContext context) {print("TestWidget build"); return Text("TestWidget"); } } class TestElement extends Element { Element _child; TestElement(Widget widget) : super(widget); @override bool get debugDoingBuild => throw UnimplementedError(); @override void performRebuild() {var _build = build(); /// Update the child view _child = updateChild(_child, _build, slot); } @override void update(Widget newWidget) { super.update(newWidget); print("TestWidget update"); / / / update performRebuild (); } @override TestWidget get widget => super.widget as TestWidget; /// Call the TestWidget build function Widget build() => widget.build(this); }Copy the code

Then drop it into main.dart as shown

The final effect is shown here

There it is. Let’s just summarize. What have you learned so far?

  • The Widget creates an Element object (calling createElement is not a Widget, it’s the Framework)
  • Widgets don’t actually control the UI
  • An Element re-calls the Widget’s build function at update time to build a sub-widget
  • UpdateChild generates a new Element based on the incoming Widget
  • The Widget function build, which passes in the context, is actually the Element object it creates. So why? On the one hand, it can isolate some Element details and avoid the uncertainty caused by frequent Widget calls or misoperations. On the other hand, the context can store the structure of the tree to find elements from the tree.

In fact, the Widget is the Element configuration information, which is frequently created and destroyed in the Dart VIRTUAL machine. Due to the large amount, a layer of Element is abstrused to read the configuration information, a layer of filtering is done, and then the actual drawing is done. The benefit of this is to avoid unnecessary refresh. Let’s take a closer look at Element

Element class structure

Before we dive into Element, let’s also look at its overall structure

As you can see, the two main abstractions of Element are:

  • ComponentElement
  • RenderObjectElement

What do they do? And then we call performRebuild, and then we call _firstBuild, and then we call performRebuild, and then we call performRebuild. We also know from the above that performRebuild eventually calls updateChild to draw the UI and RenderObjectElement is a little more complicated, it creates the RenderObject, RenderObjectWidget’s createRenderObject method, and we know from previous studies that RenderObject is actually the object that draws the UI, So let’s think of RenderObjectElement as a direct way to manipulate RenderObject, a more direct way to control the UI.

Deeper understanding of Element

In most cases, we developers do not directly manipulate Element, but it is crucial to understand the FlutterUI framework globally, especially in state management frameworks such as providers that customize their own Element implementations. So what do we need to know about this? One of the most important things to know is the lifecycle, and only by knowing the right lifecycle can you do the right thing at the right time

To verify the figure, let’s add the log print with the following code:

/// Create a LifecycleElement to implement the lifecycle function
class LifecycleElement extends TestElement{
  
  LifecycleElement(Widget widget) : super(widget);

  @override
  void mount(Element parent, newSlot) {
    print("LifecycleElement mount");
    super.mount(parent, newSlot);
  }

  @override
  void unmount() {
    print("LifecycleElement unmount");
    super.unmount();
  }

  @override
  void activate() {
    print("LifecycleElement activate");
    super.activate();
  }

  @override
  void rebuild() {
    print("LifecycleElement rebuild");
    super.rebuild();
  }

  @override
  void deactivate() {
    print("LifecycleElement deactivate");
    super.deactivate();
  }

  @override
  void didChangeDependencies() {
    print("LifecycleElement didChangeDependencies");
    super.didChangeDependencies();
  }

  @override
  void update(Widget newWidget) {
    print("LifecycleElement update");
    super.update(newWidget);
  }

  @override
  Element updateChild(Element child, Widget newWidget, newSlot) {
    print("LifecycleElement updateChild");
    return super.updateChild(child, newWidget, newSlot);
  }

  @override
  void deactivateChild(Element child) {
    print("LifecycleElement deactivateChild");
    super.deactivateChild(child); }}class TestWidget extends Widget {

  @override
  Element createElement() {
    /// Pass yourself in and have Element call the build function below
    /// Update TestElement to LifecycleElement
    return LifecycleElement(this);
  }
  /// This context is actually Element
  Widget build(BuildContext context) {
    return Text("TestWidget"); }}Copy the code

Then modify main.dart, as follows

Bool isShow = true; // add variable control isShow? TestWidget() : Container(), /// change floatingActionButton to the implementation onPressed: () {setState(() {isShow =! isShow; }); },Copy the code

Run the project to see the log

  • Call the element. The mount (parentElement, newSlot)
  • Call the update (Widget newWidget)
  • Call updateChild(Element Child, Widget newWidget, newSlot)

And then we click on the button

  • Call to deactivate ()
  • Call to unmount to ()

Let’s click the button again

Only mount this time, why? Since the Widget itself is immutable, I judge that this is the cause, but how to judge? Debug code can be added to the Framework layer of the Flutter. Let’s add the log as follows:

/// The widget base class actually has a canUpdate function. Static bool canUpdate(Widget oldWidget, Widget newWidget) { if(oldWidget.toString()=="TestWidget") { print("canUpdate${oldWidget.runtimeType == newWidget.runtimeType && oldWidget.key == newWidget.key}"); } return oldWidget.runtimeType == newWidget.runtimeType && oldWidget.key == newWidget.key; }Copy the code

It’s a static function, so it must have been called inside an Element, so let’s look for it

@mustCallSuper void update(covariant Widget newWidget) { if (newWidget.toString() == "TestWidget") { print("TestWidget update start"); } assert(_debugLifecycleState == _ElementLifecycle.active && widget ! = null && newWidget ! = null && newWidget ! = widget && depth ! = null && _active && Widget.canUpdate(widget, newWidget)); assert(() { _debugForgottenChildrenWithGlobalKey.forEach(_debugRemoveGlobalKeyReservation); _debugForgottenChildrenWithGlobalKey.clear(); return true; } ()); if (newWidget.toString() == "TestWidget") { print("TestWidget:${newWidget.hashCode}"); } _widget = newWidget; }Copy the code

The above code is the source code of Element, which calls the canUpdate function. If the update is not needed, the execution is interrupted. Let’s run the demo again and add a print to verify what the newWidget looks like. Newwidget.tostring () == “TestWidget” is added here, mainly to filter out the garbage log and re-run the project. As shown in figure

Click the back button

Then click on

CanUpdate is not called, so how do we get it to reload back? Let’s look at the data and modify the example

  @override
  void mount(Element parent, newSlot) {
    print("LifecycleElement mount");
    super.mount(parent, newSlot);
    assert(_child == null);
    print("LifecycleElement firstBuild");
    performRebuild();
  }
Copy the code

Add performRebuild() to the mount function, which will eventually trigger updateChild. Add an assert to prevent it from firing updateChild multiple times when it is loaded in again. Then modify main.dart

@override Widget build(BuildContext context) { // This method is rerun every time setState is called, for instance as done // by the _incrementCounter method above. // // The Flutter framework has been optimized to make rerunning build methods // fast, so that you can just rebuild anything that needs updating rather // than having to individually change instances of widgets. return Scaffold( appBar: AppBar( // Here we take the value from the MyHomePage object that was created by // the App.build method, and use it to set our appbar title. title: Text(widget.title), ), body: Center( // Center is a layout widget. It takes a single child and positions it // in the middle of the parent. child: isShow ? TestWidget() : Container(), ), floatingActionButton: FloatingActionButton( onPressed: () { setState(() { isShow = ! isShow; }); }, tooltip: 'Increment', child: Icon(Icons.add), ), // This trailing comma makes auto-formatting nicer for build methods. ); }Copy the code

Drop the Column, because we did not handle the index logic of the widget, so it is not normal in the Column, we will see why later, first look at the life cycle callback

First run

Click on the button

Again, why didn’t our claim work? Why is there firstBuild again? Since TestWidget is not const, it is created again after setState, and the corresponding Element is also created with a new value, resulting in re-execution. The TestWidget is not the last one, so let’s add const

/// add the current widget hashcode output, @override void mount(Element parent, newSlot) {print("LifecycleElement Widget hashCode ${widget. hashcode}"); print("LifecycleElement hashcode${this.hashCode}"); print("LifecycleElement mount"); super.mount(parent, newSlot); assert(_child == null); print("LifecycleElement firstBuild"); performRebuild(); }Copy the code

The final (start, click the button twice effect) run effect is as follows:

Running the Widget twice is consistent, which avoids Widget rebuilds

summary

After testing, we found that:

  • Widgets can be created for reuse and are decorated with const
  • The Element is not being reused because isShow is false and deactivate it, then unmount it and remove it from the Element tree.
  • Why didn’t you see the activate all the way? Shouldn’t it be part of the life cycle? And that’s where the Key comes in, and we’ll look at that in more detail later in the course when we talk about the Key.

conclusion

In this installment we have a detailed look at the Widget and Element, but it also has a State class (the core implementation of StatefulWidget) and a RenderObject class, which I’ll look at next time.