Widgets
What are the widget building blocks?
The main entry points are
RenderObjectWidget
,StatefulWidget
, andStatelessWidget
. Widgets that export data to one or more descendant widgets (via notifications or another mechanism) utilizeProxyWidget
or its subclasses,InheritedWidget
andParentDataWidget
.In general, widgets either directly or indirectly configure render objects; these indirect “component” widgets (e.g., stateful and stateless widgets) participate in a building process, whereas render object widgets manage an associated render object directly (creating it and updating it via
RenderObjectWidget.createRenderObject
andRenderObjectWidget.updateRenderObject
, respectively).Certain widgets wrap (i.e., reproject) a child widget (
ProxyWidget
), introducing heritable state (InheritedWidget
,InheritedModel
) or setting parent data used by an enclosing widget (ParentDataWidget
).ProxyWidget
notifies clients (viaProxyElement.notifyClients
) in response to widget changes (viaProxyElement.updated
, called byProxyElement.update
).ParentDataWidget
utilizes this flow to update the first descendant render object’s parent data (viaParentDataElement._applyParentData
, which callsRenderObjectElement._updateParentData
); this will be triggered any time the widget is updated.
PreferredSizeWidget
is not widely used but an interesting example of adapting widgets to a specific need (e.g., anAppBar
expressing a size preference).Numerous helper subclasses exist, most notably including
SingleChildRenderObjectWidget
,MultiChildRenderObjectWidget
,LeafRenderObjectWidget
. These provide storage for a child / children without implementing the underlying child model.Anonymous widgets can be created using
Builder
andStatefulBuilder
.
How does building work?
Every widget is associated with an element, a mutable object corresponding to the widget’s instantiation within the tree. Only those widgets associated with
ComponentElement
(e.g.,StatelessWidget
,StatefulWidget
,ProxyWidget
) participate in the build process;RenderObjectWidget
subclasses, generally associated withRenderObjectElements
, do not (these simply update their render object when building).ComponentElement
instances only have a single child, typically that returned by their widget’s build method.When the element tree is first affixed to the render tree via
RenderObjectToWidgetAdapter.attachToRenderTree
, theRenderObjectToWidgetElement
(itself aRootRenderObjectElement
) assigns aBuildOwner
for the element tree. TheBuildOwner
is responsible for tracking dirty elements (BuildOwner.scheduleBuildFor
), establishing build scopes wherein elements can be rebuilt / descendent elements can be marked dirty (BuildOwner.buildScope
), and unmounting inactive elements at the end of a frame (BuildOwner.finalizeTree
). It also maintains a reference to the rootFocusManager
and triggers reassembly after a hot reload.When a
ComponentElement
is mounted (e.g., after being inflated), an initial build is performed immediately (viaComponentElement._firstBuild
, which callsComponentElement.rebuild
).Later, elements can be marked dirty using
Element.markNeedsBuild
. This is invoked any time the UI might need to be updated reactively (or explicitly, in response toState.setState
). This method adds the element to the dirty list and, viaBuildOwner.onBuildScheduled
, schedules a frame viaSchedulerBinding.ensureVisualUpdate
.Other operations trigger a rebuild directly (i.e., without marking the tree dirty first). These include
ProxyElement.update
,StatelessElement.update
,StatefulElement.update
, andComponentElement.mount
(for the initial build, only).In contrast, builds are scheduled by
State.setState
,Element.reassemble
,Element.didChangeDependencies
,StatefulElement.activate
, etc.Proxy elements use notifications to indicate when underlying data has changed. In the case of
InheritedElement
, each dependant’sElement.didChangeDependencies
is invoked which, by default, marks that element as dirty.
Once per frame,
BuildOwner.buildScope
will walk the element tree in depth-first order, only considering those nodes that have been marked dirty. By locking the tree and iterating in depth first order, any nodes that become dirty while rebuilding must necessarily be lower in the tree; this is because building is a unidirectional process -- a child cannot mark its parent as being dirty. Thus, it is not possible for build cycles to be introduced and it is not possible for elements that have been marked clean to become dirty again.As the build progresses,
ComponentElement.performRebuild
delegates to theComponentElement.build
method to produce a new child widget for each dirty element. Next,Element.updateChild
is invoked to efficiently reuse or recreate an element for the child. Crucially, if the child’s widget hasn’t changed, the build is immediately cut off. Note that if the child widget did change andElement.update
is needed, that child will itself be marked dirty, and the build will continue down the tree.Each
Element
maintains a map of allInheritedElement
ancestors at its location. Thus, accessing dependencies from the build process is a constant time operation.If
Element.updateChild
invokesElement.deactivateChild
because a child is removed or moved to another part of the tree,BuildOwner.finalizeTree
will unmount the element if it isn’t reintegrated by the end of the frame.
How do stateful widgets work?
StatefulWidget
is associated withStatefulElement
, aComponentElement
that is almost identical toStatelessElement
. The key difference is that theStatefulElement
references theState
of the correspondingStatefulWidget
, and invokes lifecycle methods on that instance at key moments. For instance, whenStatefulElement.update
is invoked, theState
instance is notified viaState.didUpdateWidget
.StatefulElement
creates the associatedState
instance when it is constructed (i.e., inStatefulWidget.createElement
). Then, when theStatefulElement
is built for the first time (viaStatefulElement._firstBuild
, called byStatefulElement.mount
),State.initState
is invoked. Crucially,State
instance and theStatefulWidget
share the same element.Since
State
is associated with the underlyingStatefulElement
, if the widget changes, provided thatStatefulElement.updateChild
is able to reuse the same element (because the widget’s runtime type and key both match),State
will be preserved. Otherwise, theState
will be recreated.
Why is changing tree depth expensive?
Changing the depth necessarily causes a rebuild of the subtree, since there will never be an element match for the new child (since it wasn’t a child previously).
This requires
Element.inflateWidget
to be invoked, which causes a fresh build, which causes layout, paint, etc.Adding a
GlobalKey
to the previous child can mitigate this issue sinceElement.updateChild
is able to reuse elements that are stored in theGlobalKey
registry (allowing that subtree to simply be reinserted instead of rebuilt).
Last updated