flutter, inside [flutter/lib/src]
Recently I developed a pure flutter application which related to AI virtual figure interaction. During the development process, I was impressed by the performance and flexibility of the Flutter framework. The plugins are mature and easy to use, and process is smooth (the overall core features are implemented within 2 months). This experience motivated me to explore the Flutter framework more deeply, especially the code under flutter/lib/src.
Directory Overview
The flutter/lib/src directory contains the core implementation of the Flutter framework. It includes various subdirectories and files that define widgets, rendering, animation, gestures, and other essential components of Flutter. Key subdirectories include:
widgets: Contains the implementation of Flutter’s widget system, including basic widgets likeContainer,Row,Column, and more complex widgets likeListViewandGridView.rendering: Contains the rendering engine of Flutter, which is responsible for drawing widgets on the screen.animation: Contains classes and functions related to animations in Flutter, includingAnimationController,Tween, and various animation widgets.gestures: Contains classes and functions for handling user input and gestures, such as taps, swipes, and drags.painting: Contains classes and functions for drawing graphics, text, and images on the screen.foundation: Contains foundational classes and utilities used throughout the Flutter framework, such asChangeNotifier,Key, andDiagnosticable.
Core Concepts
In the above directories, two of them are particularly interesting: widgets and rendering.
- The
widgetsdirectory defines the high-level building blocks of Flutter applications:- The widgets. Each widget is a class that extends the
Widgetbase class, which serves as the blueprint for creating UI components. - The elements. Widgets are either stateless or stateful. but both types will create an Element when they are inserted into the widget tree. The Element is responsible for managing the widget’s lifecycle and states in the widget tree.
- The widgets. Each widget is a class that extends the
- The
renderingdirectory defines the low-level rendering engine of Flutter:- The RenderObject. Each RenderObject is responsible for laying out and painting a portion of the UI. RenderObjects are organized in a tree structure, similar to widgets and elements.
- The layout and painting process. The rendering engine uses a two-pass layout and painting process to determine the size and position of each RenderObject and to draw them on the screen.
The element acts as a bridge between the widget and render object trees. When a widget is inserted into the widget tree, it creates an element that manages its lifecycle and state. The element then creates a corresponding render object that is responsible for laying out and painting the widget on the screen. All widgets have a corresponding element, depending on the type of widget, which creates either a stateless, stateful, or render object element.

- The
stateless elementis created by stateless widgets, which do not have mutable state. The stateless element is responsible for building the widget tree and managing the lifecycle of the widget. - The
stateful elementis created by stateful widgets, which have mutable state. The stateful element is responsible for managing the state of the widget and rebuilding the widget tree when the state changes. Typical examples of such widgets includeStatefulWidget,Checkbox, andTextField. - The
render object elementis created by widgets that have a corresponding render object. The render object element is responsible for creating and managing the render object and updating it when the widget’s properties change. Typical examples of such widgets includeContainer,Text, andImage.
Initialization Process
When a Flutter application starts, the following steps occur:
- The
main()function is called, which typically callsrunApp()with the root widget of the application. - The
runApp()function creates aWidgetsFlutterBindinginstance, which initializes the Flutter framework and sets up the necessary bindings between the framework and the underlying platform. - A
RenderViewis created, which serves as the root of the render object tree. TheRenderViewis responsible for managing the layout and painting of the entire application. - The root widget is inserted into the widget tree, which creates the corresponding element and render object. More specifically, the
_rebuildandinflateWidgetmethod is called from the root element, which recursively builds the widget tree and creates the corresponding elements and render objects. - The layout and painting process is initiated, which determines the size and position of each render object and draws them on the screen. (The layout and painting process will be further discussed in the later sections.)
The whole process can be further summarized as the chart below

Notably, based on the Framework.dart, the initialization process for ComponentElement and RenderObjectElement is slightly different. Both of the two types if the elements start with the common updateChild method in the base Element class:
Element? updateChild(Element? child, Widget? newWidget, Object? newSlot) {
if (newWidget == null) {
if (child != null) {
deactivateChild(child);
}
return null;
}
final Element newChild;
if (child != null) {
bool hasSameSuperclass = true;
assert(() {
final int oldElementClass = Element._debugConcreteSubtype(child);
final int newWidgetClass = Widget._debugConcreteSubtype(newWidget);
hasSameSuperclass = oldElementClass == newWidgetClass;
return true;
}());
if (hasSameSuperclass && child.widget == newWidget) {
// ... ignore the lengthy element reusing logic here,
// https://github.com/flutter/flutter/blob/master/packages/flutter/lib/src/widgets/framework.dart#L3982
} else {
deactivateChild(child);
assert(child._parent == null);
// The [debugProfileBuildsEnabled] code for this branch is inside
// [inflateWidget], since some [Element]s call [inflateWidget] directly
// instead of going through [updateChild].
newChild = inflateWidget(newWidget, newSlot);
}
} else {
// The [debugProfileBuildsEnabled] code for this branch is inside
// [inflateWidget], since some [Element]s call [inflateWidget] directly
// instead of going through [updateChild].
newChild = inflateWidget(newWidget, newSlot);
}
assert(() {
if (child != null) {
_debugRemoveGlobalKeyReservation(child);
}
final Key? key = newWidget.key;
if (key is GlobalKey) {
assert(owner != null);
owner!._debugReserveGlobalKeyFor(this, newChild, key);
}
return true;
}());
return newChild;
}
For initial builds, which will call the inflateWidget method to create a new element based on the type of the widget, here the behavior diverges for different types of elements on the createElement and mount
Element inflateWidget(Widget newWidget, Object? newSlot) {
try {
// ... Timeline tracking code omitted for brevity ...
final Element newChild = newWidget.createElement(); <<<<<<<<
assert(() {
_debugCheckForCycles(newChild);
return true;
}());
newChild.mount(this, newSlot); <<<<<<<<
assert(newChild._lifecycleState == _ElementLifecycle.active);
return newChild;
} finally {
if (isTimelineTracked) {
FlutterTimeline.finishSync();
}
}
}
- For ComponentElement, the
mountmethod is called to insert the element into the widget tree, which then calls theinflateWidgetmethod to build the widget tree and create the corresponding elements and render objects.
abstract class ComponentElement extends Element {
@override
void mount(Element? parent, Object? newSlot) {
super.mount(parent, newSlot);
assert(_child == null);
assert(_lifecycleState == _ElementLifecycle.active);
_firstBuild(); <<<< which eventually calls build();
assert(_child != null);
}
@override
@pragma('vm:notify-debugger-on-exception')
void performRebuild() {
Widget? built;
try {
// ...
built = build(); <<<< build() is called here
} 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.
super.performRebuild(); // clears the "dirty" flag
}
try {
_child = updateChild(_child, built, slot); <<<< recursive call to `updateChild`
assert(_child != null);
} catch (e, stack) {
// ...
}
}
}
- For RenderObjectElement, the
mountmethod is also called to insert the element into the widget tree, but it directly creates the render object and attaches it to the parent render object.
abstract class RenderObjectElement extends Element {
@override
void mount(Element? parent, Object? newSlot) {
super.mount(parent, newSlot);
assert(() {
_debugDoingBuild = true;
return true;
}());
_renderObject = (widget as RenderObjectWidget).createRenderObject(this); <<<<<< 创建render object
assert(!_renderObject!.debugDisposed!);
assert(() {
_debugDoingBuild = false;
return true;
}());
assert(() {
_debugUpdateRenderObjectOwner();
return true;
}());
assert(slot == newSlot);
attachRenderObject(newSlot); <<<<<< attach to parent render object, 构建render object tree
super.performRebuild(); // clears the "dirty" flag
}
}
class MultiChildRenderObjectElement extends Element {
@override
void mount(Element? parent, Object? newSlot) {
super.mount(parent, newSlot);
final MultiChildRenderObjectWidget multiChildRenderObjectWidget =
widget as MultiChildRenderObjectWidget;
// <<<<<<<< create children elements for each child widget
final List<Element> children = List<Element>.filled(
multiChildRenderObjectWidget.children.length,
_NullElement.instance,
);
Element? previousChild;
// iterate through each child widget to create corresponding element,inflateWidget is called here
for (int i = 0; i < children.length; i += 1) {
final Element newChild = inflateWidget(
multiChildRenderObjectWidget.children[i],
IndexedSlot<Element?>(i, previousChild),
);
children[i] = newChild;
previousChild = newChild;
}
_children = children;
}
}
Several Types Of Trees in Flutter

So far based on the above code we have walked through the initialization process of a Flutter application, and we can summarize that there are several types of trees in the Flutter framework:
- The Widget Tree: This tree represents the hierarchical structure of widgets in a Flutter application. Each widget is a node in the tree, and the tree is built using the
build()method of widgets. - The Element Tree: This tree represents the hierarchical structure of elements in a Flutter application. Each element corresponds to a widget in the widget tree and manages its lifecycle and state.
- The Render Object Tree: This tree represents the hierarchical structure of render objects in a Flutter application. Each render object corresponds to an element in the element tree and is responsible for laying out and painting the widget on the screen.
The RenderObject
In the above three trees, the render object tree is the lowest level tree, which is responsible for the actual rendering of the UI (layout/paint) as well as hitTesting. It is kept as long as possible to avoid unnecessary rebuilds. Once the renderObject is created, it is attached to the renderObject tree:
abstract class RenderObjectElement extends Element {
@override
void mount(Element? parent, Object? newSlot) {
super.mount(parent, newSlot);
// ...
_renderObject = (widget as RenderObjectWidget).createRenderObject(this);
// ...
attachRenderObject(newSlot);
super.performRebuild(); // clears the "dirty" flag
}
@override
void attachRenderObject(Object? newSlot) {
/// insert renderObject to its ancestor renderObject
_ancestorRenderObjectElement?.insertRenderObjectChild(renderObject, newSlot);
/// retrieve the "ParentDataElements" which provides parentData information such as alignment, flex, etc.
/// and further apply the parentData to the renderObject, which will be used during layout phase.
final List<ParentDataElement<ParentData>> parentDataElements =
_findAncestorParentDataElements();
for (final ParentDataElement<ParentData> parentDataElement in parentDataElements) {
_updateParentData(parentDataElement.widget as ParentDataWidget<ParentData>);
}
}
}
The RenderObject class does not define how the child is retained, instead its subclasses define how to manage their children. It also does not declare implementations for inserting, removing, or moving children; instead, these behaviors are provided by mixins that are applied to specific render object subclasses.

For example, the ContainerRenderObjectMixin mixin provides default implementations for managing children in render boxes that have multiple children.
class MultiChildRenderObjectElement extends RenderObjectElement {
@override
void insertRenderObjectChild(RenderObject child, IndexedSlot<Element?> slot) {
final ContainerRenderObjectMixin<RenderObject, ContainerParentDataMixin<RenderObject>>
renderObject = this.renderObject;
assert(renderObject.debugValidateChild(child));
renderObject.insert(child, after: slot.value?.renderObject);
assert(renderObject == this.renderObject);
}
}
mixin ContainerRenderObjectMixin<
ChildType extends RenderObject,
ParentDataType extends ContainerParentDataMixin<ChildType>
> {
void insert(ChildType child, {ChildType? after}) {
// ...
_insertIntoChildList(child, after: after);
}
}
Similarly, the RenderObject class does not define coordination system, instead its subclass RenderBox defines a 2D coordinate system for layout and painting. Most of the time, the RenderBox is treated as the base class for custom render objects in Flutter. Since it is a very important class in the entire flutter framework, it is good to to go through part of its declaration here:
parentDatarelated, thesetupParentDatamust be called before the_updateParentDataof theRenderObjectElementto ensure that the parent data is correctly set up for the child render object.
/// Data for use by the parent render object.
///
/// The parent data is used by the render object that lays out this object
/// (typically this object's parent in the render tree) to store information
/// relevant to itself and to any other nodes who happen to know exactly what
/// the data means. The parent data is opaque to the child.
///
/// * The parent data field must not be directly set, except by calling
/// [setupParentData] on the parent node.
/// * The parent data can be set before the child is added to the parent, by
/// calling [setupParentData] on the future parent node.
/// * The conventions for using the parent data depend on the layout protocol
/// used between the parent and child. For example, in box layout, the
/// parent data is completely opaque but in sector layout the child is
/// permitted to read some fields of the parent data.
ParentData? parentData;
/// Override to setup parent data correctly for your children.
///
/// You can call this function to set up the parent data for child before the
/// child is added to the parent's child list.
void setupParentData(covariant RenderObject child);
- child management related,
void adoptChild(RenderObject child);
void dropChild(RenderObject child);
void visitChildren(RenderObjectVisitor visitor);
which further is further overridden by subclasses/mixins such as ContainerRenderObjectMixin to provide concrete implementations for managing multiple children.
/// Adds the given child to the end of the child list.
///
/// The child's [parentData] must have been set up correctly beforehand by
/// calling [setupParentData].
void add(RenderBox child) {
insert(child, after: _lastChild);
}
/// Removes the given child from the child list.
void remove(RenderBox child) {
final BoxParentData childParentData = child.parentData! as BoxParentData;
assert(childParentData.previousSibling != null || childParentData.nextSibling != null || _firstChild == child);
_removeFromChildList(child);
childParentData.previousSibling = null;
childParentData.nextSibling = null;
}
- pipelineOwner, which manages the rendering pipeline, including layout, painting, and compositing.
/// The pipeline owner for this render box.
///
/// The pipeline owner manages the rendering pipeline, including layout,
/// painting, and compositing.
PipelineOwner get owner => _owner!;
PipelineOwner? _owner;
/// Mark this render object as attached to the given owner.
///
/// Typically called only from the [parent]'s [attach] method, and by the
/// [owner] to mark the root of a tree as attached.
///
/// Subclasses with children should override this method to
/// [attach] all their children to the same [owner]
/// after calling the inherited method, as in `super.attach(owner)`.
void attach(PipelineOwner owner);
/// Mark this render object as detached from its [PipelineOwner].
///
/// Typically called only from the [parent]'s [detach], and by the [owner] to
/// mark the root of a tree as detached.
///
/// Subclasses with children should override this method to
/// [detach] all their children after calling the inherited method,
/// as in `super.detach()`.
void detach();
- layout and constraints, the
constraintis set during layout(), called by the parent render object.
Constraints? _constraints;
/// Whether this [RenderObject] is a known relayout boundary.
///
/// A relayout boundary is a [RenderObject] whose parent does not rely on the
/// child [RenderObject]'s size in its own layout algorithm. In other words,
/// if a [RenderObject]'s [performLayout] implementation does not ask the child
/// for its size at all, **the child** is a relayout boundary.
///
/// which affects how layout invalidation is propagated through the render tree.
bool? _isRelayoutBoundary;
/// Mark this render object's layout information as dirty, and either register
/// this object with its [PipelineOwner], or defer to the parent, depending on
/// whether this object is a relayout boundary or not respectively.
///
void markNeedsLayout();
- layer related
/// Whether this render object repaints separately from its parent.
///
/// Override this in subclasses to indicate that instances of your class ought
/// to repaint independently. For example, render objects that repaint
/// frequently might want to repaint themselves without requiring their parent
/// to repaint.
///
/// See [RepaintBoundary] for more information about how repaint boundaries function.
bool get isRepaintBoundary => false;
final LayerHandle<ContainerLayer> _layerHandle = LayerHandle<ContainerLayer>();
/// Mark this render object as having changed its visual appearance.
///
/// See also:
///
/// * [RepaintBoundary], to scope a subtree of render objects to their own
/// layer, thus limiting the number of nodes that [markNeedsPaint] must mark
/// dirty.
void markNeedsPaint()
/// Paint this render object into the given context at the given offset.
///
/// Subclasses should override this method to provide a visual appearance
/// for themselves. The render object's local coordinate system is
/// axis-aligned with the coordinate system of the context's canvas and the
/// render object's local origin (i.e, x=0 and y=0) is placed at the given
/// offset in the context's canvas.
///
/// Do not call this function directly. If you wish to paint yourself, call
/// [markNeedsPaint] instead to schedule a call to this function. If you wish
/// to paint one of your children, call [PaintingContext.paintChild] on the
/// given `context`.
///
/// When painting one of your children (via a paint child function on the
/// given context), the current canvas held by the context might change
/// because draw operations before and after painting children might need to
/// be recorded on separate compositing layers.
void paint(PaintingContext context, Offset offset);
/// An estimate of the bounds within which this render object will paint.
/// Useful for debugging flags such as [debugPaintLayerBordersEnabled].
///
/// These are also the bounds used by [showOnScreen] to make a [RenderObject]
/// visible on screen.
Rect get paintBounds;
- lastly, hit testing related, regarding the
hitTest, it will be discussed in another article.
/// Override this method to implement hit testing for your render object.
///
/// The default implementation does nothing and returns false.
///
/// The given position is in the local coordinate system of this render
/// object (i.e. with the origin at the top left of this render object).
///
/// If this render object hits at the given position, it should add itself
/// to the given [HitTestResult], and then typically it should also
/// hit test its children by calling their [hitTest] methods.
///
/// Returns true if the hit test was successful (i.e. if this render object
/// or one of its descendants added itself to the [HitTestResult]).
bool hitTest(HitTestResult result, {required Offset position});
The Update Of Trees
Similar to the initialization process, when a widget needs to be updated (for example, when the state of a stateful widget changes), the following steps occur:
- The
setState()method is called on the stateful widget, which marks the widget as dirty and schedules a rebuild. - The framework calls the
performRebuild()method on the corresponding element, which calls the build()method to rebuild the widget tree.
Here the element tree / render object tree update process is similar to the initialization process. When the platformDispatcher.onDrawFrame event is triggered, the _handlePersistentFrameCallback cached in the persistentFrameCallbacks set of the WidgetsBinding is called, which eventually calls the flushBuild() method to rebuild the widget tree.
void _handlePersistentFrameCallback(Duration timeStamp) {
drawFrame();
_scheduleMouseTrackerUpdate();
}
Inside the drawFrame(); the layout/layering/painting process will be further triggered. we will talk about it in the next section. The take away from this section is that the construction of Widget/Element/RenderObject trees and the layout/layering/painting process are two separate processes in the Flutter framework. The former is responsible for defining the structure and behavior of the UI, while the latter is responsible for rendering the UI on the screen, which is closely tied to different platform’s rendering pipelines.
Tasks Happening Each Frame
All tasks are executed by the scheduler, in the concept of a frame. In each frame, several phases are defined and tasks are performed in specific phases to ensure that the UI is updated and rendered correctly. These tasks are summarized in the chart below:
Timeline:
|-------- handleBeginFrame --------|-------- handleDrawFrame --------|
| # 1. in beginFrame() # 3. in drawFrame()
| Transient Callbacks | Persistent Callbacks
| - Ticker callbacks | - WidgetsBinding.drawFrame
| - AnimationController updates | - buildScope.flushBuild
| - Custom frame callbacks | - pipelineOwner.flushLayout
| | - pipelineOwner.flushPaint
| # 2. via dart vm, between beginFrame and drawFrame
| MidFrame Microtasks | # 4. also in drawFrame()
| - Futures resolved |Post Frame Callbacks
| - setState from animations | - Cleanup
| | - Schedule next frame
In the source code of scheduler.dart, we can see that the SchedulerPhase enum defines the different phases of a frame:
enum SchedulerPhase {
/// No frame is being processed. Tasks (scheduled by
/// [SchedulerBinding.scheduleTask]), microtasks (scheduled by
/// [scheduleMicrotask]), [Timer] callbacks, event handlers (e.g. from user
/// input), and other callbacks (e.g. from [Future]s, [Stream]s, and the like)
/// may be executing.
idle,
/// The transient callbacks (scheduled by
/// [SchedulerBinding.scheduleFrameCallback]) are currently executing.
///
/// Typically, these callbacks handle updating objects to new animation
/// states.
///
/// See [SchedulerBinding.handleBeginFrame].
transientCallbacks,
/// Microtasks scheduled during the processing of transient callbacks are
/// current executing.
///
/// This may include, for instance, callbacks from futures resolved during the
/// [transientCallbacks] phase.
midFrameMicrotasks,
/// The persistent callbacks (scheduled by
/// [SchedulerBinding.addPersistentFrameCallback]) are currently executing.
///
/// Typically, this is the build/layout/paint pipeline. See
/// [WidgetsBinding.drawFrame] and [SchedulerBinding.handleDrawFrame].
persistentCallbacks,
/// The post-frame callbacks (scheduled by
/// [SchedulerBinding.addPostFrameCallback]) are currently executing.
///
/// Typically, these callbacks handle cleanup and scheduling of work for the
/// next frame.
///
/// See [SchedulerBinding.handleDrawFrame].
postFrameCallbacks,
}
The Rendering Process
Let’s continue to explore the rendering process, which is related to two frame methods bind to platformDispatcher:
mixin SchedulerBinding on BindingBase {
@protected
void ensureFrameCallbacksRegistered() {
platformDispatcher.onBeginFrame ??= _handleBeginFrame;
platformDispatcher.onDrawFrame ??= _handleDrawFrame;
}
}
The _handleBeginFrame is mainly responsible for updating animations (transient frame callbacks), e.g. the _tick in animationController:
// Ticker 注册瞬态回调
class Ticker {
void scheduleTick({bool rescheduling = false}) {
_animationId = SchedulerBinding.instance.scheduleFrameCallback(
_tick, // ← 这个回调在 handleBeginFrame 中执行
rescheduling: rescheduling,
);
}
void _tick(Duration elapsed) {
// 更新动画值
_onTick?.call(elapsed);
// 继续下一帧
scheduleTick(rescheduling: true);
}
}
// AnimationController 使用 Ticker
class AnimationController {
void _tick(Duration elapsed) {
// 计算动画进度
_value = lerpDouble(_lowerBound, _upperBound, progress);
// 通知监听器
notifyListeners();
}
}
The _handleDrawFrame is responsible for the main rendering process, which eventually calls the drawFrame() method of WidgetsBinding to perform the build/layout/paint pipeline. This is where the focus of the rest of this section lies.
mixin RendererBinding on SchedulerBinding {
@protected
void drawFrame() {
rootPipelineOwner.flushLayout();
rootPipelineOwner.flushCompositingBits();
rootPipelineOwner.flushPaint();
if (sendFramesToEngine) {
for (final RenderView renderView in renderViews) {
renderView.compositeFrame(); // this sends the bits to the GPU
}
rootPipelineOwner.flushSemantics(); // this sends the semantics to the OS.
_firstFrameSent = true;
}
}
}