Widgets

What are the widget building blocks?

  • The main entry points are RenderObjectWidget, StatefulWidget, and StatelessWidget. Widgets that export data to one or more descendant widgets (via notifications or another mechanism) utilize ProxyWidget or its subclasses, InheritedWidget and ParentDataWidget.

  • 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 and RenderObjectWidget.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 (via ProxyElement.notifyClients) in response to widget changes (via ProxyElement.updated, called by ProxyElement.update).

    • ParentDataWidget utilizes this flow to update the first descendant render object’s parent data (via ParentDataElement._applyParentData, which calls RenderObjectElement._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., an AppBar 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 and StatefulBuilder.

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 with RenderObjectElements, 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, the RenderObjectToWidgetElement (itself a RootRenderObjectElement) assigns a BuildOwner for the element tree. The BuildOwner 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 root FocusManager and triggers reassembly after a hot reload.

  • When a ComponentElement is mounted (e.g., after being inflated), an initial build is performed immediately (via ComponentElement._firstBuild, which calls ComponentElement.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 to State.setState). This method adds the element to the dirty list and, via BuildOwner.onBuildScheduled, schedules a frame via SchedulerBinding.ensureVisualUpdate.

    • Other operations trigger a rebuild directly (i.e., without marking the tree dirty first). These include ProxyElement.update, StatelessElement.update, StatefulElement.update, and ComponentElement.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’s Element.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 the ComponentElement.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 and Element.update is needed, that child will itself be marked dirty, and the build will continue down the tree.

  • Each Element maintains a map of all InheritedElement ancestors at its location. Thus, accessing dependencies from the build process is a constant time operation.

  • If Element.updateChild invokes Element.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 with StatefulElement, a ComponentElement that is almost identical to StatelessElement. The key difference is that the StatefulElement references the State of the corresponding StatefulWidget, and invokes lifecycle methods on that instance at key moments. For instance, when StatefulElement.update is invoked, the State instance is notified via State.didUpdateWidget.

  • StatefulElement creates the associated State instance when it is constructed (i.e., in StatefulWidget.createElement). Then, when the StatefulElement is built for the first time (via StatefulElement._firstBuild, called by StatefulElement.mount), State.initState is invoked. Crucially, State instance and the StatefulWidget share the same element.

  • Since State is associated with the underlying StatefulElement, if the widget changes, provided that StatefulElement.updateChild is able to reuse the same element (because the widget’s runtime type and key both match), State will be preserved. Otherwise, the State 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 since Element.updateChild is able to reuse elements that are stored in the GlobalKey registry (allowing that subtree to simply be reinserted instead of rebuilt).

Last updated