How to refresh a page of Flutter application? When asked this question, the first thing that comes to mind is setState. Yes, the setState method does make the State build method go back and refresh the interface. But have you ever wondered why setState can trigger rebuild? How is the rebuild rear view updated?

With these questions in mind, start exploring the source code with purpose!

1. What does setState do

State -> setState

[->flutter/src/widgets/framework.dart]

void setState(VoidCallback fn) {
  assert(fn ! =null);
  assert(() {
    if (_debugLifecycleState == _StateLifecycle.defunct) {
     ///.
    }
    if(_debugLifecycleState == _StateLifecycle.created && ! mounted) {///.
    }
    return true; } ());final dynamic result = fn() as dynamic;
  assert(() {
    if (result is Future) {
     ///.
    return true; } ()); _element.markNeedsBuild(); }Copy the code

Tap into the setState method and you’ll see a bunch of assert’s that will eventually call the _element.markNeedsbuild () method.

StatefulElement -> markNeedsBuild

[->flutter/src/widgets/framework.dart]

void markNeedsBuild() {
  assert(_debugLifecycleState ! = _ElementLifecycle.defunct);if(! _active)return;
  assert(owner ! =null);
  assert(_debugLifecycleState == _ElementLifecycle.active);
	///.
  if (dirty)
    return;
  _dirty = true;
  owner.scheduleBuildFor(this);
}
Copy the code

The StatefulElement does not override the markNeedsBuild method itself, so we end up calling the markNeedsBuild method in Element. As you can see, there are also a bunch of assertions that determine whether the current element is marked as dirty or not, and then call owner.scheduleBuildFor(this).

BuildOwner -> scheduleBuildFor

[->flutter/src/widgets/framework.dart]

void scheduleBuildFor(Element element) {
  assert(element ! =null);
  assert(element.owner == this);
  ///.
  if(! _scheduledFlushDirtyElements && onBuildScheduled ! =null) {
    _scheduledFlushDirtyElements = true;
    onBuildScheduled();
  }
  _dirtyElements.add(element);
  element._inDirtyList = true;
  ///.
}
Copy the code

The scheduleBuildFor method first calls the onBuildScheduled method and then puts element in _dirtyElements and marks it as dirty. Just adding to the dirty list doesn’t really do anything, so the logic for actively triggering the refresh should be in the onBuildScheduled method.

/// Called on each build pass when the first buildable element is marked
/// dirty.
VoidCallback onBuildScheduled;
Copy the code

OnBuildScheduled is a callback, and we need to trace where it came in.

WidgetsBinding -> initInstances

[->flutter/src/widgets/binding.dart]

void initInstances() {
  super.initInstances();
  _instance = this;

  assert(() {
    _debugAddStackFilters();
    return true; } ());// Initialization of [_buildOwner] has to be done after
  // [super.initInstances] is called, as it requires [ServicesBinding] to
  // properly setup the [defaultBinaryMessenger] instance.
  _buildOwner = BuildOwner();
  buildOwner.onBuildScheduled = _handleBuildScheduled;
  window.onLocaleChanged = handleLocaleChanged;
  window.onAccessibilityFeaturesChanged = handleAccessibilityFeaturesChanged;
  SystemChannels.navigation.setMethodCallHandler(_handleNavigationInvocation);
  FlutterErrorDetails.propertiesTransformers.add(transformDebugCreator);
}
Copy the code

We traced it back to where BuildOwner was created, which is the initialization method for the WidgetsBinding. The onBuildScheduled call ends up calling WidgetsBinding’s _handleBuildScheduled method.

WidgetsBinding -> _handleBuildScheduled

[->flutter/src/widgets/binding.dart]

void _handleBuildScheduled() {
  // If we're in the process of building dirty elements, then changes
  // should not trigger a new frame.
 	///.
  ensureVisualUpdate();
}
Copy the code

There is also a long assertion above the _handleBuildScheduled method that will eventually call the ensureVisualUpdate method.

SchedulerBinding -> ensureVisualUpdate

[->flutter/src/scheduler/binding.dart]

void ensureVisualUpdate() {
  switch (schedulerPhase) {
    case SchedulerPhase.idle:
    case SchedulerPhase.postFrameCallbacks:
      scheduleFrame();
      return;
    case SchedulerPhase.transientCallbacks:
    case SchedulerPhase.midFrameMicrotasks:
    case SchedulerPhase.persistentCallbacks:
      return; }}Copy the code

This method determines the current phase and executes the scheduleFrame method if it is idle or in the next frame callback state, otherwise it does nothing.

SchedulerBinding -> scheduleFrame

[->flutter/src/scheduler/binding.dart]

void scheduleFrame() {
  if(_hasScheduledFrame || ! framesEnabled)return;
  ///.
  ensureFrameCallbacksRegistered();
  window.scheduleFrame();
  _hasScheduledFrame = true;
}
Copy the code

This method will call ensureFrameCallbacksRegistered method in the first place. The window’s scheduleFrame method is then called.

SchedulerBinding -> ensureFrameCallbacksRegistered

void ensureFrameCallbacksRegistered() {
  window.onBeginFrame ?? = _handleBeginFrame;window.onDrawFrame ?? = _handleDrawFrame; }Copy the code

This method registers the onBeginFrame and onDrawFrame callbacks under the window.

Window -> scheduleFrame

[->sky_engine/ui/window.dart]

/// Requests that, at the next appropriate opportunity, the [onBeginFrame]
/// and [onDrawFrame] callbacks be invoked.
///
/// See also:
///
///  * [SchedulerBinding], the Flutter framework class which manages the
///    scheduling of frames.
void scheduleFrame() native 'PlatformConfiguration_scheduleFrame';
Copy the code

When you get here, you’re stuck in the native layer. We can see from the comments that the onBeginFrame and onDrawFrame callbacks will be called.

SchedulerBinding -> _handleBeginFrame

[->flutter/src/scheduler/binding.dart]

void _handleBeginFrame(Duration rawTimeStamp) {
  if (_warmUpFrame) {// Return if the current frame has been processed
    assert(! _ignoreNextEngineDrawFrame); _ignoreNextEngineDrawFrame =true;// Ignore subsequent drawFrame
    return;
  }
  handleBeginFrame(rawTimeStamp);
}
Copy the code

If the current frame has not been processed, the handleBeginFrame method is called

SchedulerBinding -> handleBeginFrame

void handleBeginFrame(Duration? rawTimeStamp) {
  Timeline.startSync('Frame', arguments: timelineArgumentsIndicatingLandmarkEvent); _firstRawTimeStampInEpoch ?? = rawTimeStamp;// Adjust the current frame timestamp
  _currentFrameTimeStamp = _adjustForEpoch(rawTimeStamp ?? _lastRawTimeStamp);
  if(rawTimeStamp ! =null)
    _lastRawTimeStamp = rawTimeStamp;
  ///.

  assert(schedulerPhase == SchedulerPhase.idle);// You can proceed only if the current progress is idle
  _hasScheduledFrame = false;
  try {
    // TRANSIENT FRAME CALLBACKS
    Timeline.startSync('Animate', arguments: timelineArgumentsIndicatingLandmarkEvent);
    _schedulerPhase = SchedulerPhase.transientCallbacks;// Update progress
    final Map<int, _FrameCallbackEntry> callbacks = _transientCallbacks;
    _transientCallbacks = <int, _FrameCallbackEntry>{};
    / / callback
    callbacks.forEach((int id, _FrameCallbackEntry callbackEntry) {
      if(! _removedIds.contains(id)) _invokeFrameCallback(callbackEntry.callback, _currentFrameTimeStamp! , callbackEntry.debugStack); }); _removedIds.clear(); }finally {
    _schedulerPhase = SchedulerPhase.midFrameMicrotasks;// Update progress}}Copy the code

As you can see, this method is mainly used to make some adjustments and then call back the current progress.

SchedulerBinding -> _handleDrawFrame

[->flutter/src/scheduler/binding.dart]

void _handleDrawFrame() {
  if (_ignoreNextEngineDrawFrame) {
    _ignoreNextEngineDrawFrame = false;
    return;
  }
  handleDrawFrame();
}
Copy the code

If the frame is not ignored, it is called to the handleDrawFrame.

SchedulerBinding -> handleDrawFrame

void handleDrawFrame() {
  assert(_schedulerPhase == SchedulerPhase.midFrameMicrotasks);
  Timeline.finishSync(); // end the "Animate" phase
  try {
    // PERSISTENT FRAME CALLBACKS
    _schedulerPhase = SchedulerPhase.persistentCallbacks;
    ///Persistent frame callback
    for (final FrameCallback callback in_persistentCallbacks) _invokeFrameCallback(callback, _currentFrameTimeStamp!) ;// POST-FRAME CALLBACKS
    _schedulerPhase = SchedulerPhase.postFrameCallbacks;
    final List<FrameCallback> localPostFrameCallbacks =
        List<FrameCallback>.from(_postFrameCallbacks);
    _postFrameCallbacks.clear();    //postFrameCallback is cleared when called
    for (final FrameCallback callback inlocalPostFrameCallbacks) _invokeFrameCallback(callback, _currentFrameTimeStamp!) ; }finally {
    _schedulerPhase = SchedulerPhase.idle;// Update the status
    Timeline.finishSync(); // end the Frame
   ///.
    _currentFrameTimeStamp = null; }}Copy the code

You can see that this method is also used for callbacks.

At this point, the onBuildScheduled callback is complete. Element is added to _dirtyElements, and the setState method is executed.

summary

Looking at the list of calls above, we can see that setState essentially calls Element’s markNeedsBuild method. This method attempts to trigger the scheduling of frames and then adds the current element to BuildOwner’s dirty list. The data in the dirty list is updated to the screen after a series of dispatches.

How do I refresh a StatelessWidget

I’ve been asked this question before in an interview. I didn’t read the source code for Flutter very deeply at the time, so my answer was to use an external refresh. Now that I think about it, the answer is a little superficial.

Although the StatelessWidget is stateless by design and does not expose any refresh methods, it is not impossible to want to refresh it. We already know that the essence of setState refreshing a StatefulWidget is to invoke Element’s markNeedsBuild method to trigger an update, and the StatelessWidget has an Element of its own. So we can refresh a StatelessWidget by calling the markNeedsBuild method on Element of the StatelessWidget.

Verify the code:

class RefreshStateless extends StatelessWidget {
  StatelessElement element;
  String text = 'test';

  @override
  Widget build(BuildContext context) {
    element = context;
    return GestureDetector(
      onTap: () {
        text = 'I'm refreshed.'; element.markNeedsBuild(); }, child: Container( child: Text(text), ), ); }}Copy the code

In the build method we save the StatelessWidget’s BuildContext, also known as StatelessElement. We started with the test word. After clicking, we update the text to “I’m refreshed” and manually call the markNeedsBuild method.

The initial value displayed after the initial run causes us to set:

Click it and it will refresh as follows:

You can see that the refresh succeeded. The StatelessWidget can also be refreshed if we change the description in the Build method, which is not recommended.

3. How is the dirty list updated

We have already seen that the essence of setState is to add the current element to the dirty list, and the data in the dirty list will be scheduled for processing later. Now let’s track down the dirty list to see how it’s handled.

BuildOwner -> buildScope

First under BuildOwner we find that the dirty list is processed under buildScope, and then trace the call to that method:

WidgetsBinding -> drawFrame

void drawFrame() {
///.
try {
      if(renderViewElement ! =null)
        buildOwner.buildScope(renderViewElement);
      super.drawFrame();//call super
      buildOwner.finalizeTree();
    }
  ///.
}
Copy the code

The WidgetsBinding drawFrame method calls this method, and then the parent class’s drawFrame method is called. Now we need to look at how the WidgetsBinding drawFrame method is called:

You can see that it’s only called in one place.

RendererBinding -> _handlePersistentFrameCallback

void _handlePersistentFrameCallback(Duration timeStamp) {
  drawFrame();/ / call drawFrame
  _scheduleMouseTrackerUpdate();
}
Copy the code

Continue tracing the call:

RendererBinding -> initInstances

void initInstances() {
  super.initInstances();
  _instance = this;
  _pipelineOwner = PipelineOwner(
    onNeedVisualUpdate: ensureVisualUpdate,
    onSemanticsOwnerCreated: _handleSemanticsOwnerCreated,
    onSemanticsOwnerDisposed: _handleSemanticsOwnerDisposed,
  );
  window. onMetricsChanged = handleMetricsChanged .. onTextScaleFactorChanged = handleTextScaleFactorChanged .. onPlatformBrightnessChanged = handlePlatformBrightnessChanged .. onSemanticsEnabledChanged = _handleSemanticsEnabledChanged .. onSemanticsAction = _handleSemanticsAction; initRenderView(); _handleSemanticsEnabledChanged();assert(renderView ! =null);
  addPersistentFrameCallback(_handlePersistentFrameCallback);// Persistent callback
  initMouseTracker();
}
Copy the code

We can see that this method is registered as a persistent listener and is called every time a frame is updated. This callback is called after the Window shceduleFrame callback is triggered by the handleDrawFrame method that implements SchedulerBInding.

Now we know the order of method calls for dirty list updates:

BuildOwner -> buildScope

void buildScope(Element context, [ VoidCallback callback ]) {
  if (callback == null && _dirtyElements.isEmpty)
    return;
  ///. Assert part
  Timeline.startSync('Build', arguments: timelineArgumentsIndicatingLandmarkEvent);//开始Build
  try {
    _scheduledFlushDirtyElements = true;
    if(callback ! =null) {
     	///.
      _dirtyElementsNeedsResorting = false;
      try {
        callback();
      } finally {
        ///. a
      }
    }
    _dirtyElements.sort(Element._sort);// Build from top to bottom in order of depth
    _dirtyElementsNeedsResorting = false;
    int dirtyCount = _dirtyElements.length;
    int index = 0;
    while (index < dirtyCount) {
      ///. assert
      try {
        _dirtyElements[index].rebuild();// Call the rebuild method on dirty Element
      } catch (e, stack) {
        /// . error handle
      }
      index += 1;
      if (dirtyCount < _dirtyElements.length || _dirtyElementsNeedsResorting) {
        // New dirty list data may be processed here after build
        _dirtyElements.sort(Element._sort);
        _dirtyElementsNeedsResorting = false;
        dirtyCount = _dirtyElements.length;/ / dirtyCount adjustment
        while (index > 0 && _dirtyElements[index - 1].dirty) {
          // It is possible for previously dirty but inactive widgets to move right in the list.
          // We therefore have to move the index left in the list to account for this.
          // We don't know how many could have moved. However, we do know that the only possible
          // change to the list is that nodes that were previously to the left of the index have
          // now moved to be to the right of the right-most cleaned node, and we do know that
          // all the clean nodes were to the left of the index. So we move the index left
          // until just after the right-most clean node.
          index -= 1; }}}///.
  } finally {
    for (final Element element in _dirtyElements) {
      assert(element._inDirtyList);
      element._inDirtyList = false;
    }
    _dirtyElements.clear();
    _scheduledFlushDirtyElements = false;
    _dirtyElementsNeedsResorting = null;
    Timeline.finishSync();
   ///.
  }
  assert(_debugStateLockLevel >= 0);
}
Copy the code

The buildScope method reorders the _dirtyElements list data and calls the rebuild method from top to bottom.

Element -> rebuild

void rebuild() {
  ///. assert
  performRebuild();// Where the build is actually executed
 ///. assert
}
Copy the code

The rebuild method is just a bunch of judgments and assertions, and finally leaves the processing logic to the performRebuild method. Different elements have different processing logic. Here’s a look at the StatefulElement implementation.

StatefulElement -> performRebuild

void performRebuild() {
  if (_didChangeDependencies) {
    _state.didChangeDependencies();
    _didChangeDependencies = false;
  }
  super.performRebuild();
}
Copy the code

If the dependency changes, state’s didChangeDependencies are called back, and the performRebuild method of the parent class is called.

ComponentElement -> performRebuild

void performRebuild() {
  if(! kReleaseMode && debugProfileBuildsEnabled) Timeline.startSync('${widget.runtimeType}',  arguments: timelineArgumentsIndicatingLandmarkEvent);

  assert(_debugSetAllowIgnoredCallsToMarkNeedsBuild(true));
  Widget built;
  try {
    assert(() {
      _debugDoingBuild = true;
      return true; } ()); built = build();Call the build method
    assert(() {
      _debugDoingBuild = false;
      return true; } ()); debugWidgetBuilderValue(widget, built); }catch (e, stack) {
    ///.
  } finally {
    // We delay marking the element as clean until after calling build() so
    // that attempts to markNeedsBuild() during build() will be ignored.
    _dirty = false;
    assert(_debugSetAllowIgnoredCallsToMarkNeedsBuild(false));
  }
  try {
    _child = updateChild(_child, built, slot);/ / update the element
    assert(_child ! =null);
  } catch (e, stack) {
   ///.
  if(! kReleaseMode && debugProfileBuildsEnabled) Timeline.finishSync(); }Copy the code

Here we can see that the Build method is called, the new Widget configuration is generated, and then the Element is updated with the updateChild method. The implementation of the updateChild method inherits from Element.

Element -> updateChild

Element updateChild(Element child, Widget newWidget, dynamic newSlot) {
  if (newWidget == null) {
    if(child ! =null)
      deactivateChild(child);
    return null;
  }
  Element newChild;
  if(child ! =null) {
    bool hasSameSuperclass = true;
    assert(() {
      final int oldElementClass = Element._debugConcreteSubtype(child);
      final int newWidgetClass = Widget._debugConcreteSubtype(newWidget);
      ///On hot Reload, element type changes can occur
      hasSameSuperclass = oldElementClass == newWidgetClass;
      return true; } ());if (hasSameSuperclass && child.widget == newWidget) {
      // Element type Consistent Configurations consistent
      if(child.slot ! = newSlot) updateSlotForChild(child, newSlot); newChild = child; }else if (hasSameSuperclass && Widget.canUpdate(child.widget, newWidget)) {
       // Consistent Element type The configuration type is consistent with the key
      if(child.slot ! = newSlot) updateSlotForChild(child, newSlot); child.update(newWidget);/ / update the widget
      assert(child.widget == newWidget);
      assert(() {
        child.owner._debugElementWasRebuilt(child);
        return true; } ()); newChild = child; }else {
      deactivateChild(child);
      assert(child._parent == null);
      // Create new elementnewChild = inflateWidget(newWidget, newSlot); }}else {
     // Create new element
    newChild = inflateWidget(newWidget, newSlot);
  }
	///.
  return newChild;
}
Copy the code

The logic of this code is not complicated. If the newWidget is null, the Element’s configuration is gone and the element needs to be removed from the tree. If the previous child is empty, the configuration of the newWidget is loaded and an Element is generated to return. Otherwise the child can be updated. A new Element is created only if it cannot be updated.

StatefulElement -> update

void update(StatefulWidget newWidget) {
  super.update(newWidget);
  assert(widget == newWidget);
  final StatefulWidget oldWidget = _state._widget;
  _dirty = true;
  _state._widget = widget as StatefulWidget;
  try {
    _debugSetAllowIgnoredCallsToMarkNeedsBuild(true);
    final dynamic debugCheckForReturnedFuture = _state.didUpdateWidget(oldWidget) as dynamic;/ / callback didUpdateWidget
   ///.
  } finally {
    _debugSetAllowIgnoredCallsToMarkNeedsBuild(false);
  }
  rebuild();//rebuild
}
Copy the code

StatefulElement calls back the didUpdateWidget life cycle in the Update method and rebuilds immediately.

RenderBinding -> drawFrame

void drawFrame() {
  assert(renderView ! =null);
  pipelineOwner.flushLayout();
  pipelineOwner.flushCompositingBits();
  pipelineOwner.flushPaint();
  if (sendFramesToEngine) {
    renderView.compositeFrame(); // Where to actually render
    pipelineOwner.flushSemantics(); // this also sends the semantics to the OS.
    _firstFrameSent = true; }}Copy the code

And finally here, this is where you actually build the view. PipelineOwner performs a series of operations such as layout, blending, and drawing. Finally, a frame is actually rendered using the RenderView’s compositeFrame.

BuildOwner -> finalizeTree

void finalizeTree() {
  Timeline.startSync('Finalize tree', arguments: timelineArgumentsIndicatingLandmarkEvent);
  try {
    lockState(() {
      // Unmount the inactive element
      _inactiveElements._unmountAll(); // this unregisters the GlobalKeys
    });
    ///.
  } catch (e, stack) {
    ///.
  } finally{ Timeline.finishSync(); }}Copy the code

This method seems very long and actually only does one thing in non-Debug mode. That is, release the inactive element.

4, summarize

I don’t want to write it anymore, so let’s just complete it. Here is to complete the flow chart of setState:

Let’s look at dirty list refresh related calls: