As mentioned earlier, Flutter is actually a UI library written by Dart with its own rendering engine. We describe our view through the Widget, and Flutter uses its rendering engine to draw our interface based on our Widget tree. Note that drawing the interface against the Widget tree, rather than directly from the Widget tree, is an important concept that we’ll explore slowly.

What is it?

Let’s take a look at the structure of the Flutter:

The Flutter provides several layers of abstraction between us and the Rendering engine. The Widget library is the main part of our daily development, Rendering, which does some Rendering related abstractions, and dart: UI is the last layer of code written in Dart, which implements some glue code that interacts with the underlying engine. The Canvas API we use is also defined here.

When we combine good we Widget tree, Flutter from the root node to leaf node passed their constraints or call configuration, constraint limits the minHeight, minWidth, maxHeight and maxWidth and so on. For example, the Center passes the Center constraint to its child widgets. When the leaf node is accessed, all widgets in the Widget tree know their constraint, and they can determine their actual size and location based on the constraint, and so on. With a linear time complexity, the entire interface is determined on which pixel the elements are drawn.

Here’s an image I found from Google:

So what is the Widget tree drawn on the screen, if not the Widget tree that we wrote in our code? As I said before, there are not only widgets in The Flutter, but there are other object types that we care about in our daily development tasks as developers, so the statement “Everything is Widget” is true. The other object type we will mention here is RenderObject. This class is also exposed to us, but is mostly used within the Flutter framework, and most of our development will not encounter it. As you might guess from the name, they are related to rendering. Indeed, the RenderObject in The Flutter is responsible for the actual drawing on the screen, and each Widget has a corresponding RenderObject. That is, in addition to the Widget tree, we will also have a RenderObject tree.

The Dart code that we’re writing, the widgets that we’re putting together, is basically a sketch for the RenderObject, a description of the UI, and then the RenderObject draws our interface based on that information. RenderObject has methods such as performLayout, paint, and these methods are responsible for drawing on the screen. The Widget concept we use gives us a nice abstraction on the RenderObject. We just have to declare what we want. So some of you might be thinking, well, why don’t we just do it without widgets? Most people don’t want to work directly with the underlying drawing, because they have to calculate where each pixel should be drawn, and it will be a lot more work, just like when we were developing An Android app, we didn’t use OpenGL to draw all the interfaces, we used various views, viewgroups, Widgets, like Views, are abstractions that frameworks give us to write interfaces.

theRenderObjectWhat did you do?

Renderobjects don’t have any state, they don’t contain any business logic, they only know a little bit about their parent RenderObject, and they have the ability to access their child RenderObject. They don’t work with each other at the app level, they don’t make decisions for other people, they just draw on the screen in order.

Widgets return other widgets in their build methods, causing the widget tree to grow larger and larger. At the very bottom of the tree you will encounter one or more RenderObjectWidget classes, which create renderObjects for the entire Widget tree.

We mentioned earlier that widgets are given constraints to determine their size, but these constraints are given renderObjects that determine the actual physical size of the Widget on screen based on the constraints. Different renderObjects determine their size in different ways. There are three main categories:

  • Take up as much space as you can, likeCenterThe correspondingRenderObject
  • With the childWidgetKeep it the same size, for exampleOpacityThe correspondingRenderObject
  • Certain size, for exampleImageThe correspondingRenderObject

These three points are important for the RenderObject that Flutter comes with, and we generally do not customize the RenderObject.

I also have a brother:Element

Take a look at our first diagram of the Flutter structure. Our Widget layer abstracts a Widget tree, our DART: UI does the actual drawing, abstracts a RenderObject tree, and what does the middle Rendering do? It also abstracts a tree: the Element tree.

When a Widget’s build method is called, the Flutter calls Widget.createElement(this) to create an Element. This Widget is the original configuration of this Element, and this Element will hold a reference to it. It’s worth noting that the State objects associated with our StatefulWidget are actually managed by The Element, since states tend to live for a long time and widgets can be built frequently. The Element, in turn, has a significant difference from the Widget in that it updates, and when the build method is called again, it updates its reference to the new Widget. We already said that what we’re drawing on the screen is not a Widget tree. Now we can say what we’re drawing. It’s an Element tree. The Element tree represents the actual structure of the app, it’s the skeleton of the app, it’s what’s actually drawn on the screen. The Element queries the information carried by the Widget by reference and, after a series of decisions, gives it to the RenderObject to draw. (Mainly judge whether there is any modification, whether to redraw)

Now it’s clear:

The Element holds the reference between the Widget and the RenderObject, the RederObject is responsible for translating the upper description into something that can communicate with the underlying rendering engine, and the Element is the glue code between the Widget and the RenderObject.

Why are there three brothers?

So why on earth do we have these three layers? Can’t we just draw them? Why add this complexity? We know that Flutter is a responsive framework and that all widgets are immutable. Any modification will result in a rebuild, that is, its Widget tree will be rebuilt. Isn’t it too much for an app to build its interface millions of times a day? RenderObject is a costly object, because it is responsible for the drawing of the underlying layer, which is relatively expensive. Therefore, it is frequently destroyed and rebuilt, which will definitely affect the performance. Most of the time, only a small part of the interface is modified. If everything else is the same, our Widget tree will change whatever happens. Our app skeleton, our Element tree structure, will not need to be rebuilt at all, just redraw the parts that have changed. Widgets are just configuration files, they’re lightweight, so you can change what you want, and what we’re actually drawing on the screen is the Element, and just figure out if the Widget it points to has changed, and if it has, redraw it, and if it hasn’t, Even though we may be frequently notified of redrawing by means such as setState, and the Widget tree is frequently rebuilt, the performance of the Flutter is not affected. While we enjoy the convenience of immutable, we also reuse the objects that actually draw on the screen.

Reuse mechanism of Flutter

We said earlier that when the build method is called, Element updates the reference and decides whether to redraw. The criteria are whether the runtime type has changed, or if a Widget has a key, has the key changed, and so on. This sounds a bit abstract, but let’s actually write a little code to get a feel for the mechanics of the Flutter.

Using yesterday’s app as an example, this time we want to be able to switch the addition and subtraction buttons when we click to reset the FAB. For those of you who have not read my previous post, and for those who are not familiar with the development of Flutter, I will define a button called the FancyButton, so that you can read the code of the Flutter:

class FancyButton extends StatefulWidget {
  final Widget child;
  final VoidCallback callback;

  const FancyButton({Key key, this.child, this.callback}) : super(key: key);

  @override
  FancyButtonState createState() {
    returnFancyButtonState(); }}Copy the code

Because it is a StatefulWidget, the core logic of the Widget is in its State. The StatelessWidget is simpler. It includes a build method like this, which I won’t write here.

class FancyButtonState extends State<FancyButton> {
  @override
  Widget build(BuildContext context) {
    return Container(
      child: RaisedButton(
        color: _getColors(),
        child: widget.child,
        onPressed: widget.callback,
      ),
    );
  }

  Color _getColors() {
    return _buttonColors.putIfAbsent(this, () => colors[next(0.5)]); }}Map<FancyButtonState, Color> _buttonColors = {};
final _random = Random();

int next(int min, int max) => min + _random.nextInt(max - min);
List<Color> colors = [
  Colors.blue,
  Colors.green,
  Colors.orange,
  Colors.purple,
  Colors.amber,
  Colors.lightBlue,
];
Copy the code

In fact, we just wrapped the RaisedButton and provided the color, the rest of the configuration is upstream.

Next, we can add this button to the main page:

@override Widget build(BuildContext context) {
  final incrementButton =
      FancyButton(child: Text("Add"), callback: _incrementCounter);
  final decrementButton =
      FancyButton(child: Text("Reduce"), callback: _decrementCounter);
  List<Widget> _buttons = [incrementButton, decrementButton];
  if (_reversed) {
    _buttons = _buttons.reversed.toList();
  }

  return Scaffold(
    appBar: AppBar(
      title: Text(widget.title),
    ),
    body: Center(
      child: Column(
        mainAxisAlignment: MainAxisAlignment.center,
        children: <Widget>[
          Container(
            decoration: BoxDecoration(
                borderRadius: BorderRadius.circular(4.0),
                color: Colors.green.withOpacity(0.3)),
            child: Image.asset("qrcode.jpg"),
            margin: EdgeInsets.all(4.0),
            padding: EdgeInsets.only(bottom: 4.0),
          ),
          Text(
            'You have pushed the button this many times:',
          ),
          Text(
            '$_counter',
            style: Theme.of(context).textTheme.display1,
          ),
          Row(
              mainAxisAlignment: MainAxisAlignment.spaceAround,
              children: _buttons),
        ],
      ),
    ),
    floatingActionButton: FloatingActionButton(
      onPressed: _resetCounter,
      tooltip: 'Increment',
      child: Icon(Icons.refresh),
    ),
  );
}
Copy the code

The logic for swapping button positions is simple:

void_swap() { setState(() { _reversed = ! _reversed; }); }Copy the code

Okay, we’re ready to run the code.

Everything works as expected, the buttons are swapped and the click events are normal… Wait a minute! The color of the button didn’t move!

This is the judgment logic we mentioned earlier, reuse mechanism! Our Widget does not have a key, so it determines whether it has changed based on the runtime type. In our case, both types are the same, FancyButton. We had expected Flutter to find that the two buttons were different colors and redraw them. But the color is defined in the State, the State is not destroyed, so just based on the runtime type Element, we end up thinking that there is no change, so we see that the color is not updated, so why does the text change with the click event? Well, that’s because they’re passed in from the outside, the outside is recreated. To solve this problem, we simply add keys to the two buttons according to the rules, so the Flutter will know that our Widget is different based on the key:

List<UniqueKey> _buttonKeys = [UniqueKey(), UniqueKey()]; .final incrementButton = FancyButton(
    key: _buttonKeys.first, child: Text("Add"), callback: _incrementCounter);
final decrementButton = FancyButton(
    key: _buttonKeys.last, child: Text("Reduce"), callback: _decrementCounter);
Copy the code

There are several types of Key, but that’s not the focus of today’s discussion. The Flutter will no longer think that nothing has changed, run the project again and the background color will change as the button toggles.

Well, here we have the basic workflow of the Flutter. No wonder it builds frequently but doesn’t get stuck! Those who want to find out more can also check out this video from the Team Flutter on the Rendering process. There is a lot of information today, but the good thing is that we don’t have to deal with it directly in our daily development. We do not have to force themselves to understand at once, especially beginners friends, do not worry, although understand the principle will help us to deal with some problems, at present know that there is such a thing as a good impression, a long time will naturally understand.

Code address: counter