Flutter Interview Handbook

Custom Layouts & Drawing

100%

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 using layoutChild(id, constraints) and sets child positions using positionChild(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(): Reads constraints, calls child.layout(constraints, parentUsesSize: true) on children, and sets size = 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 like drawLine, 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 RepaintBoundary repaint 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(): Allocating Paint(), Path(), or TextStyle() objects inside paint() creates hundreds of garbage collector allocations per frame at 120 FPS. Cache Paint instances as class fields!
  • Overusing RepaintBoundary: Each RepaintBoundary consumes additional GPU memory for its layer cache. Only wrap subtrees that repaint frequently (e.g. video views, chart animations, signature pads).