Custom Layouts & Drawing
When standard layout widgets (Row, Column, Stack) or built-in container decorations are insufficient, Flutter provides low-level APIs for custom spatial positioning (CustomMultiChildLayout, custom RenderBox) and pixel-level canvas graphics (CustomPainter).
1. Custom Layouts: CustomMultiChildLayout vs Custom RenderBox
1. CustomMultiChildLayout & MultiChildLayoutDelegate
Allows custom positioning and sizing of children without creating a custom RenderObject subclass.
- Mechanism: Children are identified by IDs using
LayoutId(id: 'child1', child: Widget). - Delegate Methods:
performLayout(Size size): Queries child sizes usinglayoutChild(id, constraints)and sets child positions usingpositionChild(id, offset).shouldRelayout(delegate): Determines whether layout needs to re-run.
class RadialLayoutDelegate extends MultiChildLayoutDelegate {
final int itemCount;
RadialLayoutDelegate(this.itemCount);
@override
void performLayout(Size size) {
for (int i = 0; i < itemCount; i++) {
if (hasChild(i)) {
final childSize = layoutChild(i, BoxConstraints.loose(size));
positionChild(i, Offset(i * 20.0, i * 10.0));
}
}
}
@override
bool shouldRelayout(RadialLayoutDelegate oldDelegate) => oldDelegate.itemCount != itemCount;
}2. Custom RenderBox Subclassing
For maximum performance and full control over hit-testing and painting, subclass RenderBox directly:
performLayout(): Readsconstraints, callschild.layout(constraints, parentUsesSize: true)on children, and setssize = computeSize().paint(PaintingContext context, Offset offset): Draws child render objects or custom canvas paths.hitTestChildren()/hitTestSelf(): Handles touch gesture targeting.
2. Pixel-Level Graphics: CustomPainter Mechanics
CustomPainter renders 2D vector graphics onto a canvas during the paint phase.
Core Components
Canvas: The drawing surface interface (methods likedrawLine,drawRect,drawCircle,drawPath).Paint: Configures stroke width, color, gradients, anti-aliasing, and blend modes.Path: Vector paths created via bezier curves (cubicTo,quadraticBezierTo).
class CircleProgressPainter extends CustomPainter {
final double progress;
CircleProgressPainter(this.progress);
@override
void paint(Canvas canvas, Size size) {
final paint = Paint()
..color = Colors.blue
..strokeWidth = 8.0
..style = PaintingStyle.stroke;
final center = Offset(size.width / 2, size.height / 2);
final radius = size.width / 2;
canvas.drawCircle(center, radius, paint);
}
@override
bool shouldRepaint(CircleProgressPainter oldDelegate) => oldDelegate.progress != progress;
}3. Painting Optimization: shouldRepaint & RepaintBoundary
1. shouldRepaint() Logic
Flutter calls shouldRepaint(oldDelegate) before re-executing paint(). Returning false when properties are unchanged reuses the recorded display list, avoiding vector recalculations!
2. RepaintBoundary & Layer Compositing
By default, a widget and its parent share the same compositing layer. If a parent widget repaints, all children repaint as well.
Wrapping a CustomPaint or complex subtree in a RepaintBoundary:
- Creates a dedicated Compositing Layer on the GPU.
- Isolates repaints: Animations inside the
RepaintBoundaryrepaint only that isolated layer, leaving the rest of the screen intact!
[ Default Single Layer ] [ RepaintBoundary Layer Isolation ]
ββββββββββββββββββββββββββββ ββββββββββββββββββββββββββββ
β Screen Root Layer β β Screen Root Layer β
β ββββββββββββββββββββββββ β β ββββββββββββββββββββββββ β
β β CustomPainter (anim) β β β β Static UI β β
β ββββββββββββββββββββββββ β β ββββββββββββββββββββββββ β
β (Repaints WHOLE screen!) β ββββββββββββββββββββββββββββ€
ββββββββββββββββββββββββββββ β RepaintBoundary Layer β
β ββββββββββββββββββββββββ β
β β CustomPainter (anim) β β
β ββββββββββββββββββββββββ β
β (Repaints ONLY isolated) β
ββββββββββββββββββββββββββββ4. Trade-offs & Production Considerations
- Memory Allocation inside
paint(): AllocatingPaint(),Path(), orTextStyle()objects insidepaint()creates hundreds of garbage collector allocations per frame at 120 FPS. CachePaintinstances as class fields! - Overusing
RepaintBoundary: EachRepaintBoundaryconsumes additional GPU memory for its layer cache. Only wrap subtrees that repaint frequently (e.g. video views, chart animations, signature pads).