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: