Study the most avoid blind, no plan, fragmentary knowledge can not be strung into a system. Where did you learn, where did you forget? I can’t remember the interview. Here I have collated the most frequently asked questions in the interview and several core pieces of knowledge in the Flutter framework, which are analyzed in about 20 articles. Welcome to pay attention and make progress together. [Flutter framework]

Introduction:

The most user-related part of any front-end system is the UI. A button, a label, is displayed and interacts with the corresponding UI element. When we first learn, we tend to focus only on how to use it. However, if we only know how to use it, it is difficult for us to find solutions and ideas to solve problems, and we cannot optimize for some specific scenarios. This is the UI system for Flutter.

SetState () = setState() = setState()

2. Interview must-ask: Talk about the lifecycle of Widgets and States

3. Layout constraint principle of Flutter

15 examples of the Flutter layout process

After reading this article you will learn about the rendering mechanism of Flutter and the principle behind setState()


The introduction

When we first learned Flutter, we usually thought of calling setState() when we needed to update the page data. However, many blogs and official articles do not recommend that we use setState() on the nodes of the page because of the unnecessary overhead (only for the nodes of the page, of course, the widgets of the Flutter must be refreshed without setState()). Many state management schemes are also designed to achieve what is called “local refresh”. At this point we need to think not only why does setState() refresh the page, but why might it cause extra wear and tear? What’s the logic behind this function? This article and you reveal one by one.


Why does setState() refresh the page

1, setState ()

Our demo starts with a simple counter

Click the ➕ number at the bottom of the page, add one to the local variable, and then the setState() of the current page is called. The page is rebuilt and the displayed data is increased. From the phenomenon that the entire process will inevitably pass setState () – · · · · · · · · · · · · · · · · · · · – > the current State of the build () – · · · · · · · · · · · · · · · · – > page rendering – · · · · · · · · · · · · – > screen refresh. So what does setState() do?

State#setState(VoidCallback fn)

@protected
void setState(VoidCallback fn) {
  final dynamic result = fn() as dynamic;
  _element.markNeedsBuild();
}
Copy the code

After removing all the assertions, setState actually does two things

1, call the VoidCallback fn we passed in

2, call _element.markNeedsbuild ()


2, element. MarkNeedsBuild ()

We worked with widgets in the development of the Flutter, but there was this comment on the Widget.

Describes the configuration for an [Element].

abstract class Widget extends DiagnosticableTree {
  final Key key;
  Element createElement();
  String toStringShort() {
    return key == null ? '$runtimeType' : '$runtimeType-$key';
  }
Copy the code

A Widget is just a configuration file that describes the elements that actually manage the building, rendering, and so on of the page at the Framework level. The elements are created by the widgets and hold the Widget objects. Each Widget has one Element.

In the demo above, we called setState() on HomePageState, where the Element has its HomePage object created. HomePage(Widget) -Homepagestate (State) -HomepageElement (StatefulElement) correspond to each other.

Element#markNeedsBuild()

/// The object that manages the lifecycle of this element.
/// Responsible for managing the build and lifecycle of all elements
@override
BuildOwner get owner => _owner;

void markNeedsBuild() {
  // Mark yourself as dirty
  _dirty = true;
  owner.scheduleBuildFor(this);
}
Copy the code

Call BuildOwner. ScheduleBuildFor (element), BuildOwner here in WidgetsBinding initialization complete instantiation, is responsible for managing the widgets framework, Each Element object gets its reference from its parent after it is mounted to the Element tree.

WidgetsBinding#initInstances()

void initInstances() {
  super.initInstances();
  _instance = this; _buildOwner = BuildOwner(); buildOwner.onBuildScheduled = _handleBuildScheduled; /... /}Copy the code

BuildOwner#scheduleBuildFor(Element element)

void scheduleBuildFor(Element element) {
  // Add to the _dirtyElements collection
  _dirtyElements.add(element);
  element._inDirtyList = true;
}
Copy the code

Finally, add yourself to a dirty Element collection maintained in BuildOwner.

Summary: 1, Element: hold the Widget, store the context information, RenderObjectElement additional hold RenderObject. It is used to traverse the view tree and support the UI structure.

2. The setState() procedure simply marks the current Element as dirty (HomePageState in demo) and adds it to the _dirtyElements combination.


3. Rendering mechanism of Flutter

The above process doesn’t seem to do any rendering, so how is the page redrawn? The key is the rendering mechanism of Flutter

The Start FrameWork notifies Engine that it is ready to render, and when the next Vsync signal arrives, the Engine layer uses the Windows.onDrawFrame callback FrameWork to build and draw the entire page. (Here I wonder why the notification should originate from the Framework first, rather than from the Vsync driver directly. If a page is very slow and happens to take longer than one Vsync period to draw each frame, then each frame will not be drawn within a Vsync period. This can be avoided by sending a notification to the next Vsync signal after building and drawing on the framework guarantee. Every time the Engine receives a notification to render the page, the Engine calls Windows.onDrawFrame and eventually gives it to the _handleDrawFrame() method.

@protected
void ensureFrameCallbacksRegistered() {
  // The processing before building the frame is mainly to perform animation related calculations
  window.onBeginFrame ?? = _handleBeginFrame;//Windows.onDrawFrame is handed over to _handleDrawFrame
  window.onDrawFrame ?? = _handleDrawFrame; }Copy the code

SchedulerBinding#handleDrawFrame()

void handleDrawFrame() {
  try {
    // PERSISTENT FRAME CALLBACKS
    // Key callback
    for (FrameCallback callback in _persistentCallbacks)
      _invokeFrameCallback(callback, _currentFrameTimeStamp);
    // POST-FRAME CALLBACKS
    final List<FrameCallback> localPostFrameCallbacks =
        List<FrameCallback>.from(_postFrameCallbacks);
    _postFrameCallbacks.clear();
   for (FrameCallback callback in localPostFrameCallbacks)
        _invokeFrameCallback(callback, _currentFrameTimeStamp);
  } finally{/ · · · · · · · · · · · · · · · · · · · · · · · · · · · · · /}}Copy the code

As mentioned in the Callback Principles section of the Flutter AnimationController, these three queues are maintained in the SchedulerBinding of the Flutter

  • Transient Callbacks, called by the system’s [window.onBeginFrame] callbacks, are used to synchronize the application’s behavior to the system’s presentation. For example, the [Ticker]s and [AnimationController]s triggers come from it.
  • Persistent callbacksThe callback is triggered by the system’s [window.onDrawFrame] method. For example,The framework layer uses it to drive the render pipeline for build, Layout, and paint
  • Post-frame callbacks are used to get the render time of a frame for performance monitoring related to frame rate.

SchedulerBinding. HandleDrawFrame of _persistentCallbacks and _postFrameCallbacks set () callback. _persistentCallbacks are callbacks for fixed processes such as build, Layout, and paint. To keep track of this _persistentCallbacks collection, Found in RendererBinding. InitInstances () calls in the initialization addPersistentFrameCallback (_handlePersistentFrameCallback) method. There is only one line call to this method: drawFrame().

Conclusion:

  • SchedulerBindingIt maintains three queues: TransientCallbacks(animation processing), PersistentCallbacks(page building rendering), and PostframeCallbacks(after each frame is drawn), and calls them back when appropriate.
  • Passes after receiving a render notification from EngineWindows.onDrawFrameMethod callback to the Framework layer callhandleDrawFrame
  • handleDrawFrameThe callbackPersistentCallbacks, the final calldrawFrame()

4, drawFrame ()

The drawFrame() method is typically clicked directly into the RendererBinding

RendererBinding#drawFrame()

void drawFrame() {
  pipelineOwner.flushLayout();
  pipelineOwner.flushCompositingBits();
  pipelineOwner.flushPaint();
  renderView.compositeFrame(); // this sends the bits to the GPU
  pipelineOwner.flushSemantics(); // this also sends the semantics to the OS.
}
Copy the code

As you can see from the names of these methods, we call the layout, draw, and render frames. And looking at the class name, this is the Binding responsible for rendering and does not call the Widget build. This is because WidgetsBinding is an onRendererBinding (understood as an inherited onRendererBinding), which overwrites drawFrame() and should actually call widgetsBinding.drawFrame ().

WidgetsBinding#drawFrame()

@override
void drawFrame() {
try {
    if(renderViewElement ! =null)
      // buildOwner is the object that manages WidgetBuild
      RenderViewElement is the root node of the UI tree
      buildOwner.buildScope(renderViewElement);
    super.drawFrame();
  		// Remove inactive nodes from the UI tree
    buildOwner.finalizeTree();
  } finally{/ · · · · · · · · · · · · · · · · · /}}Copy the code

Buildowner.buildscope (renderViewElement) is called before super.drawFrame(). BuildOwner#buildScope(Element context, [ VoidCallback callback ])

void buildScope(Element context, [ VoidCallback callback ]) {
  if (callback == null && _dirtyElements.isEmpty)
    return;
  try {
    _scheduledFlushDirtyElements = true;
    _dirtyElementsNeedsResorting = false;
    _dirtyElements.sort(Element._sort);
    _dirtyElementsNeedsResorting = false;
    int dirtyCount = _dirtyElements.length;
    int index = 0;
    while (index < dirtyCount) {
      try {
        ///The key in this
        _dirtyElements[index].rebuild();
      } catch(e, stack) {/········· /}}}finally {
    for (Element element in _dirtyElements) {
      element._inDirtyList = false; } _dirtyElements.clear(); }}Copy the code

After setState(), add homePageState to _dirtyElements. This method calls rebuild() for each object in the collection. The rebuild() method eventually goes to performRebuild(), which is an abstract method in an Element.


Why does high setState () cost performance

1, performRebuild ()

Look at the implementation in CompantElement, the common ancestor of StatelessElement and StatefulElement

CompantElement#performRebuild()

void performRebuild() {
  Widget built;
  try {
    built = build();
  } catch (e, stack) {
    built = ErrorWidget.builder();
  } 
  try {
    _child = updateChild(_child, built, slot);
  } catch (e, stack) {
    built = ErrorWidget.builder();
    _child = updateChild(null, built, slot); }}Copy the code

This method calls the build method of the subclass directly and returns a Widget that corresponds to the call to the build method in the previous HomePageState().

Pass the widget from this new build() to updateChild(_Child, built, slot), along with the _child(Element type) that was previously mounted on the Element tree. The core logic of setState() is in updateChild(_child, built, slot).

2, updateChild(_child, built-in, slot)

StatefulElement#updateChild(Element child, Widget newWidget, dynamic newSlot)

@protected
Element updateChild(Element child, Widget newWidget, dynamic newSlot) {
  if (newWidget == null) {
    if(child ! =null)
      //child == null && newWidget == null
      deactivateChild(child);
    //child ! = null && newWidget == null
    return null;
  }
  if(child ! =null) {
    if (child.widget == newWidget) {
      //child ! = null && newWidget == child.widget
      if(child.slot ! = newSlot) updateSlotForChild(child, newSlot);return child;
    }
    if (Widget.canUpdate(child.widget, newWidget)) {
      if(child.slot ! = newSlot) updateSlotForChild(child, newSlot);//child ! = null && Widget.canUpdate(child.widget, newWidget)
      child.update(newWidget);
      return child;
    }
    deactivateChild(child);
  }
  // child ! = null && ! Widget.canUpdate(child.widget, newWidget)
  return inflateWidget(newWidget, newSlot);
}
Copy the code

The official comment on this method is:

newWidget == null newWidget ! = null
child == null Returns null. Returns new [Element].
child ! = null Old child is removed, returns null. Old child updated if possible, returns child or new [Element].

In general, there are four cases based on the _child that was previously mounted in the Element tree and the newWidget object that comes out of the call to build() again

  • If the previous position child is null
    • If newWidget is null, it indicates that there is no child node in this location.
    • If newWidget is not null, a new child node is added to the location and calls inflateWidget(newWidget, newSlot) to generate a new Element
  • If the previous child was not null
    • C, if newWidget is null, this location needs to be removed from the previous node deactivateChild(child)Remove and return NULL
    • D, call newWidget if it is not nullWidget.canUpdate(child.widget, newWidget)Compare whether it can be updated. This method will compare the two widgetsruntimeTypeandkeyIf it is consistent, the child Widget has not changed, but the data of the current node needs to be updated according to the newWidget(configuration list)child.update(newWidget); If the inconsistency indicates that this position has changed, thendeactivateChild(child)After the returninflateWidget(newWidget, newSlot)

And in the demo, looking at the code, we can see that

After calling setState() in homePageState, both the child and newWidget are not empty and are of the Scaffold type and since we do not have the specified key to display, So it will go to the child.update(newWidget) method ** (note that the child has become Scaffold) **.

3. Recursive updates

Update (Covariant Widget newWidget) is an abstract method. Different elements have different implementations. Take StatulElement as an example

void update(StatefulWidget newWidget) {
  super.update(newWidget);
  assert(widget == newWidget);
  final StatefulWidget oldWidget = _state._widget;
  // Notice that we mark ourselves as dirty before calling didUpdateWidget to
  // let authors call setState from within didUpdateWidget without triggering
  // asserts.
  _dirty = true;
  _state._widget = widget;
  try {
    final dynamic debugCheckForReturnedFuture = _state.didUpdateWidget(oldWidget) as dynamic;
  } finally {
    _debugSetAllowIgnoredCallsToMarkNeedsBuild(false);
  }
  rebuild();
}
Copy the code

This method first calls back _state.didupDateWidget, and we can override this method in State, only to find that rebuild() is finally called again. Note that the Scaffold that calls rebuild() is not the HomePageState but its first child. So the whole process goes back to performRebuild(), which again updates the child node by calling updateChild(_child, Built, slot). Recursion continues until you reach the most child level of the page. As shown in figure:

The build() procedure, though, simply calls a component’s constructor and does not involve mounting the Element tree. However, because we tend to have a nested combination of N widgets per component, the overhead of traversing each Scaffold is not trivial (you can count how many layers there are in that Scaffold for interest).

Back in our demo, what we’re really asking is to click the + sign to change the data that was previously displayed.

But calling setState() directly on the page node will re-call the build() method for all the widgets (including their various nesting), which can be costly if our requirement is a more complex page.

To solve this problem, see goodbye setState()! Elegant UI with Model binding Flutter DataBus used ~


conclusion

When we call setState() on a high node, we build all the widgets again, not necessarily in the Element tree, but often the widgets we use are nested with multiple other types of widgets. Each build() method eventually comes down to a lot of overhead, so doing only partial refreshes through various state management schemes, streams, etc., is a good habit to get into on a daily basis.


The last

In this installment, we examined the setState() procedure, focusing on the recursive update procedure. Just like the Android Activity or Fragment lifecycle, widgets and states in the Flutter also provide corresponding callbacks, such as initState(), build(). Who is behind the invocation of these methods, and what is the timing of their invocation? How is the Element lifecycle invoked? We will talk about it in the next issue

Finally, please pay attention to QAQ