Flutter development is based on a unique relationship between three special objects: Widget
s, Element
s, and RenderObject
s. I call this Flutter’s Holy Trinity.
To understand Widget
s, Element
s, and RenderObject
s, you must first understand the Flutter Pipeline at a high level. If you understand Flutter’s Pipeline, then you can continue to read about these three special types of objects.
Widget
s are what allow you to write simple, declarative code that renders a UI.
RenderObject
s handle all of the real UI work, like layout, painting, and hit testing.
Element
s handle all the organizational responsibilities related to connecting Widget
s to RenderObject
s, and tree traversal, such as looking up InheritedWidget
s.
Render Objects
RenderObject
s are the most foundational piece of the Holy Trinity. In fact, you could build an entire UI with just RenderObject
s - no Widget
s or Element
s needed. But the downside of a RenderObject
UI is that all of your UI code would be imperative. You would need to explicitly add children, remove children, run layout, run paint, etc. A pure RenderObject
UI would feel a lot like coding for Android and iOS.
// A barebones RenderObject structure that declares the most
// important RenderObject methods.
class RenderMyThing extends RenderBox {
@override
void performLayout() {
// TODO:
}
@override
bool hitTest(HitTestResult result, {Offset? position}) {
// TODO:
}
@override
void paint(PaintContext context, Offset position) {
// TODO:
}
@override
void insertChild(RenderObject child, Slot? slot) {
// TODO:
}
@override
void moveChild(RenderObject child, Slot? slot) {
// TODO:
}
@override
void removeChild(RenderObject child, Slot? slot) {
// TODO:
}
}
Learn more about RenderObject
responsibilities.
Widgets
It’s the current year, so we don’t want imperative UI, we want declarative UI. This means adding Widget
s. Widget
s are glorified data structures, which collect a number of properties that are used to configure an associated RenderObject
.
// A hypothetical Widget, which takes in a `color` and `shape` property, and passes them
// to an associated `RenderObject`.
class MyThing extends RenderObjectWidget {
const MyThing({
required this.color,
required this.shape,
});
final Color color;
final BoxShape shape;
@override
RenderMyThing createRenderObject(BuildContext context) {
return RenderMyThing(
color: color,
shape: shape,
);
}
@override
void updateRenderObject(BuildContext context, RenderMyThing renderObject) {
renderObject
..color = color
..shape = shape;
}
}
Elements
It’s easy for a Widget
to create a RenderObject
and pass over a few properties, so why do we need this thing called an Element?
Recall that Widget
s never change - they're immutable. When we want a Widget
property to change, we don't mutate the existing Widget
, we throw that Widget
away and replace it with an entirely new Widget
. But, RenderObject
s are expensive. We can't afford to throw away RenderObjet
s. So what happens to the RenderObject
that a Widget
creates? To solve this problem, Flutter provides us with Element
s.
An Element
is a long-lived object, which associates a Widget
with a RenderObject
. Importantly, the Widget
that’s associated with the RenderObject
can be replaced with new Widget
s, over time. The Element
is responsible for switching out the old Widget
with the new Widget
, and updating the RenderObject
based on the new Widget
.
Like RenderObject
s, Element
s are also initially created by Widget
s.
class MyThing extends RenderObjectWidget {
const MyThing({
required this.color,
required this.shape,
});
final Color color;
final BoxShape shape;
// Creates the `Element` that will hold this `Widget`. That `Element` will
// also switch out this `Widget` for different configurations of this `Widget`
// in the future, such as when `color` changes from red to green.
@override
MyThingElement createElement() => MyThingElement();
// When the `Element` is first created, it calls this method to create the
// associated `RenderObject`.
@override
RenderMyThing createRenderObject(BuildContext context) {}
// When a new `Widget` is paired with the `Element`, and a `RenderObject` already exists
// for the `Element`, the `Element` calls this method to update the `RenderObject`, which
// was created earlier with `createRenderObject()`. For example, when the `color`
// goes from red to green.
@override
void updateRenderObject(BuildContext context, RenderMyThing existingRenderObject) {}
}
You might wonder who calls createElement()
on a Widget to get this whole dance started. That would be Flutter’s BuildOwner
, which sits on top of all Widget
s, Element
s, and RenderObject
s. The BuildOwner
gets the process started, and does some internal accounting related to caching and re-using Element
s when the Widget
tree rebuilds in different configurations. The internal details of the BuildOwner
are beyond the scope of this guide.
We’ve seen a barebones representation of a RenderObject
and a Widget
. What does an Element
look like?
class MyThingElement extends RenderObjectElement {
// TODO:
}
TODO