Zero: preface

1. Series of introduction

The first thing you might think about Flutter painting is to use the CustomPaint component to create a custom CustomPainter object. All visible components of a Flutter, such as Text, Image, Switch, Slider, and so on, are drawn. However, most of the components of a Flutter are not drawn using the CustomPaint component. The CustomPaint component is a wrapper around the underlying painting of the framework. This series is an exploration of Flutter drawing, testing, debugging, and source code analysis to reveal things that are overlooked or never known when drawing, and points that can go wrong if omitted.

  • Flutter drawing exploration 1 | CustomPainter refresh the correct posture
  • Flutter drawn to explore 2 | comprehensive analysis CustomPainter related classes
  • Flutter draw explore CustomPainter class 3 | in-depth analysis

2. About State# setState

I’m just a knife, heroes can take me to exterminate evil, bad guys can take me to slaughter innocent. I will be praised for the good deeds of heroes, and I will be despised for the evil deeds of villains. However, I can not determine my own good or bad, after all, I am only a knife, a tool. I just pray to be used, that’s all. This is State#setState, a tool that triggers a refresh that is good or bad, not by itself, but by the person who uses it.

Note: there is a summary at the end of the article, pay attention to check, after all, not everyone can read the text.


First, the soldiers of the camp of iron and water

Test cases

This summary will use a test to show what changes and what does not change during a refresh in Flutter. This is crucial to understanding Flutter. The simplest StatefulWidget tested here is red, yellow, blue and green every 3 seconds.

void main() => runApp(ColorChangeWidget());

class ColorChangeWidget extends StatefulWidget {
  @override
  _ColorChangeWidgetState createState() => _ColorChangeWidgetState();
}

class _ColorChangeWidgetState extends State<ColorChangeWidget> {
  final List<Color> colors = [
    Colors.red, Colors.yellow,
    Colors.blue, Colors.green ];

  Timer _timer;
  int index = 0;
  
  @override
  void initState() {
    super.initState();
    _timer = Timer.periodic(Duration(seconds: 5), _update);
  }

  void _update(timer) {
    setState(() {
      index = (index + 1) % colors.length;
    });
  }

  @override
  void dispose() {
    _timer.cancel();
    super.dispose();
  }

  @override
  Widget build(BuildContext context) {
    returnCustomPaint( painter: ShapePainter(color: colors[index]), ); }}Copy the code

Use timer.periodic to create a Timer that fires every 3 seconds, modify the active index, and execute _ColorChangeWidgetState#setState to rebuild the interface. Again, ShapePainter draws a circle and displays it using CustomPaint.

class ShapePainter extends CustomPainter {
  final Color color;
  ShapePainter({this.color});
  @override
  voidpaint(Canvas canvas, Size size) { Paint paint = Paint().. color = color; canvas.drawCircle(Offset(100.100), 50, paint);
  }
  
  @override
  bool shouldRepaint(covariant ShapePainter oldDelegate) {
    return oldDelegate.color != color;
  }
}
Copy the code

2. Case debugging test

Now just add a breakpoint on the ShapePainter#paint method. Here’s what happens when you paint twice. One important thing to note is that while State#setstate will rebuild nodes under the current build method, RenderObject will not be rebuilt. RenderCustomPaint’s memory address is always #1dbcd. You can release the breakpoint, let the color change a few more times, and you will find that the address of the rendered object remains the same.

But one object that keeps changing is the ShapePainter object. You can also see from _ColorChangeWidgetState#build why the artboard object keeps changing, because State#setState triggers State#build, where ShapePainter is re-instantiated.

---->[_ColorChangeWidgetState#build]----
@override
Widget build(BuildContext context) {
  return CustomPaint(
    painter: ShapePainter(color: colors[index]),
  );
}
Copy the code

You may be wondering why not use ShapePainter as a member variable so that you don’t need to create it every time you build. According to the use of CustomPainter in the Flutter source code, properties in the drawboard class are defined as final (i.e. constant) for static drawing and are not allowed to change their properties. So you can’t modify the information even if ShapePainter is a member variable. At this point, CustomPainter is just a configuration description like a Widget and is lightweight.

As mentioned in the first article, for drawings with sliding or animation requirements, the frequency of the rebuild trigger is very high, and even if the object is light, it will create a large number of objects in a short period of time, which is not good. You can use the repaint property to control the refresh of the artboard, as described in Part 3. So external State#setState for static drawing will cause descriptive information objects like widgets and CustomPainter to be recreated. The RenderObject that is actually responsible for drawing and layout is still the same object, which is the soldier of the ying Pan flowing water.


What does State#setState do

1. SetState method debugging analysis

SetState is a member method in the State class that passes in a callback method. After the assertion, the callback method is executed and _element.markNeedsbuild () is executed. You can see that the setState method is basically executing this method, so what is _enement?


Each State class holds a StatefulElement and a StatefulWidget object, which in this case is the markNeedsBuild() method that executes the state-held Element.

As you can see from the variables panel, the Widget currently held by _ColorChangeWidgetState is a ColorChangeWidget and the Element held by _ColorChangeWidgetState is a StatefulElement. It is now time to call the markNeedsBuild() method of the Element object.


The next step is to go to Element.markNeedsBuild, which is the Element class. After two small judgments, the element’s _dirty attribute is set to true, which is the element label dirty. Then execute owner.scheduleBuildFor(this), where the owner object is a member of Element and its type is BuildOwner. Note that the method takes this, which is the Element itself.


The next step will enter BuildOwner scheduleBuildFor, if element _inDirtyList to true, will return directly. It is set to true only when it is added to the dirty table set, as shown in line 2590. When the conditions are met, the onBuildScheduled method is executed.


At this point the method goes to WidgetsBinding._handleBuildScheduled. So onBuildScheduled is a method member of BuildOwner that at some point was assigned WidgetsBinding._handleBuildScheduled, and that’s why it’s here. So this is just calling ensureVisualUpdate.


In SchedulerBinding. EnsureVisualUpdate method through scheduleFrame to dispatch a new frame.

This method calls window.scheduleFrame() to request new frames,

Window#scheduleFrame is a native method that, as you can see from the comments, calls the onBeginFrame and onDrawFrame callbacks at the next appropriate time.


Methods are finished, after a wave of stack back, back to BuildOwner. ScheduleBuildFor. BuildOwner has a list of _dirtyElements for storing dirty elements. The current element is then included and the _inDirtyList is set to true. So setState goes out of the stack at this point.

So State#setState basically does two things:

1Trigger frame scheduling via onBuildScheduled2, will be held by the current StateElementThe BuildOwner object joins the dirty table collection in BuildOwnerCopy the code

2. Rebuild element

Although the setState method is over, its legacy lives on. After frame scheduling is triggered, frame redrawing is triggered, and the dirty elements in the table also trigger rebuild. Remember the set of _dirtyElements dirty tables maintained in BuildOwner, the class responsible for managing and building elements, where every frame redraw goes. Now put a breakpoint on BuildOwner. BuildScope to see how the method for drawing frames is pushed.


Callback is null, and _dirtyElements is null. We know that one element was loaded into the dirty table due to the State#setState method, so we’ll continue.

The dirty element list is sorted by sort first.


This is where the _dirtyElements rebuild method is executed, and the fun is about to begin.


We at any time can not forget, to always know what this is, this is the vast sea of source code in the brightest light. The rebuild method is the StatefulElement that was added to the dirty table before, and Element. Rebuild. Because rebuild is overridden in StatefulElement, the use goes to the parent class’s methods. You can see that the rebuild method is just doing an assertion, performRebuild.


And then into the StatefulElement performRebuild, obviously, is due to the StatefulElement rewrite the method. Here’s an important point: if _didChangeDependencies is true, then _state triggers the didChangeDependencies() callback method.


As you can see, StatefulElement holds a State object, which in turn holds a StatefulElement. As you can see from the picture below, the current object type, StatefulElement and _ColorChangeWidgetState are mutually held relationships.


Here _didChangeDependencies is false, and super.performrebuild () is executed. Since the parent of StatefulElement is ComponentElement, the method for pushing it is as follows:


Moving down, you’ll see that a local variable built is initialized with the build() method. And then there’s the miracle.


Moving forward, the build method implementation is _state.build(this), and you should see what this code means. What’s going to happen next, where this current element is going to go.


The _state member here, which we already know is _ColorChangeWidgetState, so this build method, which is the build component we wrote. For the first time, there’s an intersection between the source code and what we’re writing, and the BuildContext object for the callback is that Element. As follows, CustomPaint and ShapePainter are re-instantiated in the build method. They are no longer what they used to be, they are discarded like grass, and new objects with new configuration information are added to this round of construction. ShapePainter’s color will now change as index changes.


Then after the method pops onto the stack, the Built object is assigned to the CustomPaint object that was just created and holds ShapePainter as the next color, so the built object becomes the worker with the new configuration information and starts working.


Then we come to a very core method Element#updateChild. Before getting into this method, let’s take a look at the hierarchy of the element tree. Now only three elements, elements of the tree is the top frame internal create RenderObjectToWidgetElement, the second is the current this — StatefulElement, The third is CustomPaint SingleChildRenderObjectElement created by components. If there’s any doubt about this, see canto II. Here is built through the new Widget to update _child SingleChildRenderObjectElement the _child is the third node


Now go to element. updateChild, and notice the information in the variables area.

[1]. NewWidget is the newly created worker with the new configuration information. [2]. thisIs the second element node, the caller of the updateChild method, a StatefulElement object [3]. The child is the third element nodes, the children stay updated SingleChildRenderObjectElement [4]. The return value here is for updatesthisThe _child attribute of the node, which updates the third element nodeCopy the code

If the newWidget is null, null is returned, and if the child is not null, it is removed from the tree. It’s not null here and it’s going to go down and declare a local variable newChild, where child is not null.


Child. widget holds the previous CustomPaint, newWidget is new, so this condition is not satisfied. This also shows that if the old and new Widget objects do not change, there will be optimizations to use the old child directly.


Since the old and new widgets are not the same object, the following branch is used to determine whether the Widget can be updated. The updatable condition is that the new and old components have the same runtime type and key, which is met here, moving on.


Child.update (newWidget) is then executed to update the child, the third element node, with the new configuration information.


Then enter the SingleChildRenderObjectElement. Update, will perform the super update method.


Enters RenderObjectElement. After the update, still can carry out super. Update, to reach the top Element# update, the operation is only the assignment for newWidget _widget members.


3. RenderObject updates

Then Element# update the stack, return to RenderObjectElement. The update method, performs a very important method here widget. UpdateRenderObject (this, renderObject). Hopefully, you’ve understood the previous three articles, and you’ll see better results as you scroll down.


Then will enter CustomPaint updateRenderObject method, properties of incoming renderObject reset. Then you can see that the renderObject was passed in, so the renderObject was not recreated, and the properties of the object are set. RenderObject is the camp to draw, and you just need to reset the configuration information.


At this point, do you remember what happens with Set Painter in RenderCustomPaint? Chapter three says. The _didUpdatePainter method is triggered.

ShouldRepaint is then used to determine whether redraw should be triggered when the artboard is reset. So shouldRepaint only fires when the outside world forces RenderCustomPaint to reset painter, the most common of which is the outside world’s State#setState. So shouldRepaint is guarding this door.


At the same time as the two artboards, add yourself to PipelineOwner’s list of drawings to be redrawn using markNeedsPaint.


RenderObject after the update, the method, in turn, the stack will to RenderObjectElement. The update, will _dirty set to false, then the stack.


Then SingleChildRenderObjectElement still will update its children, due to its child here is null, the method performs is null.


Third element node is updated, the method to return to ComponentElement performRebuild, at this point the _child RenderObject held by object is to use the new configuration update, and joined the stay to render a list. That is, updates using setState are lightweight configuration information innovations, while objects like Element, RenderObject, and State are not recreated, but are updated based on configuration information.


After updating, unstack to BuildOwner. BuildScope to clean the dirty table.


The next BuildOwner. BuildScope out stack RendererBinding. DrawFrame stack, after is drawn. This method should already be familiar. The pipelineowner.flushpaint () method is described in detail in article 3, which is not covered here.


Finally, ShapePainter#paint is triggered to draw. This is the Element rebuild and RenderObject update that takes place at setState. We should have seen that using setState does not normally cause Element and RenderObject to be recreated, but rather updates based on new Widget configuration information. That’s about as good as it gets.


Third, summary

1. Is State#setState really so scary?

Since the dawn of Flutter, State#setState has existed like a miracle. To refresh Flutter, use setState. So much so that State#setState has been abused and all sorts of timing updates have been created. When recognizing the local refresh of components such as ValueListenableBuilder, FutureBuilder, StreamBuilder, AnimatedBuilder, or state-management improved local refresh components such as Provider, Bloc, Seems to have made State#setState the butt of gossip, and it’s one-sided. Suffice it to say that, as at the beginning of this article, State#setState is just a tool, and tools are neither good nor bad.

As you can see from the above code, State#setState adds the holding Element to the dirty table to be built and triggers the scheduling of frames to rebuild and draw. So State#setState is good or bad depending on the level of the Element. If someone tries to use State#setState at a higher level to refresh, saying State#setState is bad is like saying the knife is bad after killing an innocent person.


2. Partial refresh, also onlysetStateThe encapsulation

The ValueListenableBuilder component is rebuilt using setState to listen for object changes.


The FutureBuilder component is rebuilt using setState based on the state of the asynchronous task.


The StreamBuilder component is rebuilt using setState based on the Stream state.


The AnimatedBuilder component also listens for the animator and is rebuilt using setState.


Even the BlocBuilder for state management Bloc relies on setState for reconstruction.


In the Provider, there is some encapsulation of the refresh, but it still comes down to element#markNeedsBuild.

So whatever local refresh, the internal principle is the same as State#setState. It’s basically a layer of encapsulation of setState. We can’t deny the value of State#setState just because we can’t see it. It’s like saying bad things about other people while keeping them working at the bottom. For setState, note that it refreshes the element hierarchy, not negates it.


3. What does Custompainter # shouldRepaint guard?

Now to finish Custompainter#shouldRepaint is only called when RenderCustomPaint sets the artboard property. RenderCustomPaint sets the artboard property in this scenario: When RenderObjectElement triggers an update, the widget#updateRenderObject method is used to set the property, not the object.

---->[CustomPaint#updateRenderObject]----
@override
voidupdateRenderObject(BuildContext context, RenderCustomPaint renderObject) { renderObject .. painter = painter .. foregroundPainter = foregroundPainter .. preferredSize = size .. isComplex = isComplex .. willChange = willChange; }Copy the code

RenderObjectElement triggers update triggers basically due to the setState method being executed. So shouldRepaint has its limits. The next article will explore redraw scenarios that shouldRepaint doesn’t regulate, and the corresponding solutions.