RenderObject
s are the most powerful, versatile, and complicated objects in a Flutter user interface. RenderObject
s play a number of roles in the Flutter Pipeline, along with Widget
s, Element
s, and Layer
s. It’s important to understand the Flutter Pipeline at a high level, before you try to understand the responsibilities of RenderObject
s, specifically.
Let’s look at a RenderObject
’s responsibilities.
Layout
A RenderObject
is responsible for layout. Layout means choosing a size, and positioning children.
Not all RenderObject
s have children. A RenderObject
with no children is referred to as a “leaf” RenderObject
. A leaf RenderObject
only needs to choose its size during layout.
An example of a RenderObject
with children is a column. The column’s RenderObject
places its children in a vertical list, taking up all space within the column. Most layouts can be accomplished with Widget
s in the Flutter framework, e.g., Row
, Column
, Stack
, Center
, Align
. However, special use-cases might require a layout policy that Flutter doesn’t support out of the box. In that case, you may need to implement a RenderObject
that positions its children as desired.
Before you implement a RenderObject
for layout, see if a CustomMultiChildLayout
might handle your needs.
Learn more about Render Object layout.
Hit Testing
A RenderObject
is responsible for determining when a tap or click falls within its bounds. A RenderObject
is also responsible for deciding whether or not that tap or click is allowed to pass through to other RenderObject
s behind it. For example, a red rectangle might absorb the hit, while a transparent rectangle might let the hit pass through to whatever sits behind it.
In most cases, a GestureDetector
widget will provide all the hit testing and gesture handling behaviors that you need. But if you’re implementing a RenderObject
for other reasons, such as layout or painting, then you’ll need to be sure to include the right implementation for hit testing, too.
Learn more about hit testing with Render Objects.
Painting
A RenderObject
is responsible for painting the content on the screen. Every piece of content that you see in a Flutter UI is painted by a RenderObject
. This includes every shape, image, and text-run. If you’ve only ever worked with Widget
s, then you may have believed that Widget
s paint the content on the screen. But that’s incorrect. Widget
s are merely configurations for RenderObject
s. Your Widget
trees create render trees, filled with RenderObject
s, which paint the screen.
Painting is accomplished with a combination of two tools: Canvas
painting and Layer
pushing.
Canvas
painting is the most common and easy-to-understand path to putting content on the screen. Every RenderObject
is given access to a Canvas
in the RenderObject
’s paint()
method. The Canvas
object offers methods like drawRect()
, drawOval()
, drawImage()
, and drawParagraph()
. These draw methods are self-explanatory.
@override
void paint(PaintingContext context, Offset position) {
// Paint a red rectangle that sits at the desired `position` and
// takes up the desired `size` of this RenderObject.
context.canvas.drawRect(
Rect.fromLTWH(position.dx, position.dy, size.width, size.height),
Paint()..color = Colors.red,
);
}
Sometimes you need control over visual composition in a way that a Canvas
can’t help you. Perhaps you need to create a dividing point in your painting so that part of your painting re-runs, but the other part doesn’t. Or, perhaps you need to paint a texture to the screen. Or, perhaps you want to blur all the content behind your RenderObject
. To accomplish these specialized tasks, you need to take advantage of an even lower-level part of Flutter called the “layer tree”. The layer tree, as the name implies, is composed of Layer
objects. RenderObject
s can “push” layers using the PaintingContext
that they’re given in the paint()
method. Understanding layers is beyond the scope of this guide, but keep in mind that RenderObject
s are responsible for pushing layers onto the layer tree, and this is part of painting.
@override
void paint(PaintingContext context, Offset position) {
// Display a texture by pushing a TextureLayer onto the layer tree.
// The texture is scaled and distorted to fit exactly the `size` of
// this Render Object.
context.pushLayer(
TextureLayer(
textureId: myTextureId,
rect: Rect.fromLTWH(position.dx, position.dy, size.width, size.height),
),
);
}
If the only thing you need to access ia a Canvas
, consider using a CustomPaint
widget instead of implementing a RenderObject
. You’ll get the same capabilities with a tiny fraction of the effort.
Learn more about painting with Render Objects.
Children
Managing children is a RenderObject
’s most complicated responsibility.
Some RenderObject
s have a single child. Some RenderObject
s have a few children with specific names, e.g., appBar
, drawer
, floatingActionButton
. Some RenderObject
s have an entire list of arbitrary children, e.g., a Column
, Row
, or Stack
. Finally, some RenderObject
s have a combination of these child organizations.
Coming from a Widget
perspective, it might seem strange that managing children would be a complicated responsibility. After all, managing children in the widget tree is extremely easy. Why is it so hard in the render tree?
Unlike Widget
s, RenderObject
s are mutable, which means they change over time. Therefore, you can’t just throw away an old list of children and replace it with a new list of children. Instead, a RenderObject
needs to remove specific children that are no longer desired, and add new children that are desired.
[INSERT IMAGE SHOWING SOME CHILDREN BEING REMOVED AND OTHERS ADDED]
Additionally, a RenderObject
has to implement the storage for children. The storage doesn’t need to be complicated, but it’s still something that a RenderObject
needs to include. For example, if a RenderObject
has a list of children, the RenderObject
needs to decide whether to store those children in a List
, or implement a linked list, or store them in a Map
, or perhaps a custom data structure. This choice depends on when, where, and how you expect your children to be added and removed over time.
[INSERT IMAGE SHOWING DIFF DATA STRUCTURES HOLDING CHILDREN]
Lastly, there are various contractual obligations that RenderObject
s have when managing children. For example, every RenderObject
has a method called visitChildren()
. This method is expected to run a callback for every one of its children. If this method isn’t implemented correctly, it completely breaks the ability for the Flutter Pipeline to use your RenderObject
. Similarly, a RenderObject
is responsible for attaching its children in attach()
and detaching its children in detach()
. A Render Object must ensure that all protocol expectations are implemented, with regard to children.
[INSERT CODE SAMPLE WITH CLASS OUTLINE SHOWING ALL CHILDREN METHOD SIGNATURES]
The organization of children within a RenderObject
is called the “child model”. There are a few child models that are very common. To help developers implement RenderObject
s with these common child models, the Flutter framework provides mixins that handle most of the protocol implementation details on your behalf.
Checkout RenderObjectWithChildMixin for single-child RenderObject
s. Checkout ContainerRenderObjectMixin for RenderObject
s with a list of arbitrary children.
Learn more about managing children in a Render Object.
Relationship to Widgets and Elements
In a typical Flutter UI, RenderObject
s don’t exist on their own. Instead, a RenderObject
is associated with a Widget
and an Element
. I call this Flutter’s Holy Trinity.
A Widget
holds the desired configuration for a RenderObject
. A Widget
also creates the desired Element
and RenderObject
. An Element
rebuilds its Widget
, when needed, and manages the relationship between the Widget
and RenderObject
. Finally, a RenderObject
has its properties updated based on its Widget
, and a RenderObject
has its children provided to it by its Element
.
[INSERT IMAGE OF TRINITY]
The relationship between Widgets
, Element
s, and RenderObject
s is complex, and it can take a while to understand. But after all the Widget
s, Element
s, and RenderObject
s do their dance, your Flutter UI ends up with an entire widget tree, element tree, and render tree, all intermingled, together.
[INSERT IMAGE OF TREES INTERMINGLED]
These relationships are what allow you to declare simple Widget
constructors that paint complex, dynamic content.
Learn more about Flutter’s Holy Trinity.