setState explained

Every Flutter Engineer has ever used setState to update the UI in his code. But not all of us know what happens behind the scenes when the method is called. In this article, let's outline the steps that follows setState method.
Content:
- setState is called
- StatefulElement's markNeedsBuild
- BuildOwner's scheduleBuildFor
- BuildScope's _scheduleBuildFor
- Next frame
setState is called
As we talked earlier in the other article, every widget has corresponding element. So does StatefulWidget has StatefulElement. When setState((){})
is called within widget, it makes some debug checks and does the job that is given as input, at the end it calls StatefulElement's markNeedsBuild method:
@protected
void setState(VoidCallback fn) {
...
final Object? result = fn() as dynamic;
...
_element!.markNeedsBuild();
}
StatefulElement's markNeedsBuild
As every element has access to BuildOwner, through StatefulElement's markNeedsBuild, element is firstly checked if it is dirty to check whether setState was called previously. If is called, next line is not reached, if not element marks itself as dirty and asks BuildOwner to add itself to the element dirty list:
abstract class Element {
bool _dirty = true;
bool _inDirtyList = false;
void markNeedsBuild() {
...
if (dirty) {
return;
}
_dirty = true;
owner!.scheduleBuildFor(this);
}
}
BuildOwner's scheduleBuildFor
Manager class BuildOwner makes some checks in debug mode, gets BuildScope of the element and calls buildScope's _scheduleBuildFor.
class BuildOwner {
void scheduleBuildFor(Element element) {
...
final BuildScope buildScope = element.buildScope;
...
buildScope._scheduleBuildFor(element);
...
}
}
BuildScope's _scheduleBuildFor
BuildScope is a class that determines the scope of a BuildOwner.buildScope operation. With the call of _scheduleBuildFor, element is added to scope's _dirtyElements, along side marking the elemet as in dirty list as well.
final class BuildScope {
final List<Element> _dirtyElements = <Element>[];
void _scheduleBuildFor(Element element) {
...
if (!element._inDirtyList) {
_dirtyElements.add(element);
element._inDirtyList = true;
}
}
}
So far we have outlined the steps from setState until the element is added to dirty list. It is time to dive into the process how elements are actually updated at the next frame.
Next frame
When engine is ready to lay out and paint next frame:
- it calls handleDrawFrame of SchedulerBinding, which is registered with PlatformDispatcher.onDrawFrame
- which calls drawFrame of WidgetsBinding
- This method does some debug checks and calls BuildOwner's buildScope method
try {
...
if (rootElement != null) {
buildOwner!.buildScope(rootElement!);
}
...
} finally {
...
}
Consequently, buildScope state set to building and _flushDirtyElements is called, where _dirtyElements list is ordered in depth order.
void buildScope(Element context, [ VoidCallback? callback ]) {
...
try {
...
buildScope._building = true;
buildScope._flushDirtyElements(debugBuildRoot: context);
} finally {
buildScope._building = false;
...
}
}
void _flushDirtyElements({ required Element debugBuildRoot }) {
_dirtyElements.sort(Element._sort);
_dirtyElementsNeedsResorting = false;
try {
for (int index = 0; index < _dirtyElements.length; index = _dirtyElementIndexAfter(index)) {
final Element element = _dirtyElements[index];
if (identical(element.buildScope, this)) {
assert(_debugAssertElementInScope(element, debugBuildRoot));
_tryRebuild(element);
}
}
} finally {
for (final Element element in _dirtyElements) {
if (identical(element.buildScope, this)) {
element._inDirtyList = false;
}
}
_dirtyElements.clear();
_dirtyElementsNeedsResorting = null;
_buildScheduled = false;
}
}
This way, only inner widgets are rebuilt first followed by outer widgets and for each element in _dirtyElements list called _tryRebuild -> rebuild -> performBuild -> build, following that each updated element's _inDirtyList is set to false as well as _dirty variable followed by _dirtyElements list is cleared, buildScope building state is set to false.
void _tryRebuild(Element element) {
...
element.rebuild();
...
}
void rebuild({bool force = false}) {
...
performRebuild();
...
}
void performRebuild() {
Widget? built;
...
try {
...
built = build();
...
} finally {
super.performRebuild(); // clears the "dirty" flag
}
...
}
@protected
@mustCallSuper
void performRebuild() {
_dirty = false;
}
References: