Flutter Performance Revealed: RepaintBoundary

Author: xuyisheng

Flutter draws Widgets on the screen. If the content of a Widget needs to be updated, it can only be redrawn. However, Flutter will also redraw some Widgets, and the contents of these Widgets are still partially unchanged. This can impact the performance of your application, sometimes dramatically. If you are looking for a way to prevent unnecessary partial repaints, you might consider utilizing RepaintBoundary.

In this blog post, we will explore RepaintBoundary in Flutter. We will see a demo of how to implement RepaintBoundary and how to use it in your flutter app.

RepaintBoundary

The RepaintBoundary class is Null safe. First, you need to understand what RepaintBoundary is in Flutter. It is a Widget that sets different display levels for its Child. This Widget sets a different display level for its Child. If a subtree is compared with its surrounding parts, it will be repainted in an unexpectedly short period of time. Flutter recommends that you use RepaintBoundary to further improve performance.

Why do you need to use RepaintBoundary?

Flutter Widgets are related to RenderObjects. A RenderObject has a function called paint, which is used to perform the painting process. Nonetheless, draw methods can be used regardless of whether the content of the associated component changes. This is because if one of the RenderObjects is set to dirty, Flutter may redraw other RenderObjects in similar layers. When a RenderObject needs to be redrawn using RenderObject.markNeedsPaint, it will suggest its closest predecessor to redraw. The ancestor will also do the same thing to its predecessors, up to the root RenderObject. When a RenderObject’s paint strategy is enabled, all its related RenderObjects in similar layers will be repainted.

And sometimes, when a RenderObject should be redrawn, other RenderObjects in similar layers should not be redrawn, because their drawing artifacts remain unchanged. So it would be better if we just do redraw on some RenderObjects. Using RepaintBoundary can help us limit the generation of markNeedsPaint on the rendering tree and limit the generation of paintChild under the rendering tree.

RepaintBoundary can decouple previous render objects from related render objects. In this way, it is possible to only redraw the subtree whose content has changed. Using RepaintBoundary can further improve the execution efficiency of the application, especially when subtrees that should not be repainted require a lot of work to repaint.

We will do a simple demonstration program, the background is drawn using CustomPainter, there are 10000 ellipses. There is also a cursor that moves after the last position the customer touched the screen. Below is the code without RepaintBoundary.

Example

In the body, we’ll create a Stack widget. Inside, we’ll add a StackFit.expand, and add two widgets: _buildBackground(), and _buildCursor(). We will define the following code.

Stack(
  fit: StackFit.expand,
  children: <Widget>[
    _buildBackground(),
    _buildCursor(),
  ],
),

_buildBackground() widget

In the _buildBackground() widget. We will return the CustomPaint() widget. Inside we will add the BackgroundColor class on the painter. We will define this below. In addition, we will add the isComplex parameter to be true, which means whether to indicate whether the painting of this layer should be cached, and willChange is false to indicate whether the raster cache should be told that this painting may change in the next frame.

Widget _buildBackground() {
  return CustomPaint(
    painter: BackgroundColor(MediaQuery.of(context).size),
    isComplex: true,
    willChange: false,
  );
}

BackgroundColor class

We will create a BackgroundColor extending CustomPainter.

import 'dart:math';
import 'package:flutter/material.dart';

class BackgroundColor extends CustomPainter {

  static const List<Color> colors = [
    Colors.orange,
    Colors.purple,
    Colors.blue,
    Colors.green,
    Colors.purple,
    Colors.red,
  ];

  Size_size;
  BackgroundColor(this._size);

  @override
  void paint(Canvas canvas, Size size) {
    final Random rand = Random(12345);

    for (int i = 0; i < 10000; i ++ ) {
      canvas.drawOval(
          Rect.fromCenter(
            center: Offset(
              rand.nextDouble() * _size.width - 100,
              rand.nextDouble() * _size.height,
            ),
            width: rand.nextDouble() * rand.nextInt(150) + 200,
            height: rand.nextDouble() * rand.nextInt(150) + 200,
          ),
          Paint()
            ..color = colors[rand.nextInt(colors.length)].withOpacity(0.3)
      );
    }
  }

  @override
  bool shouldRepaint(BackgroundColor other) => false;
}

_buildCursor() widget

In this Widget, we will return the Listener Widget. We will add the _updateOffset() component in the onPointerDown/Move method and add CustomPaint. Inside, we will add a Key and CursorPointer class. We will define below. Also, we’ll add ConstrainedBox().

Widget _buildCursor() {
  return Listener(
    onPointerDown: _updateOffset,
    onPointerMove: _updateOffset,
    child: CustomPaint(
      key: _paintKey,
      painter: CursorPointer(_offset),
      child: ConstrainedBox(
        constraints: BoxConstraints. expand(),
      ),
    ),
  );
}

CursorPointer class

We will create a CursorPointer extending CustomPainter.

import 'package:flutter/material.dart';

class CursorPointer extends CustomPainter {

  final Offset _offset;

  CursorPointer(this._offset);

  @override
  void paint(Canvas canvas, Size size) {
    canvas.drawCircle(
      _offset,
      10.0,
      new Paint()..color = Colors.green,
    );
  }

  @override
  bool shouldRepaint(CursorPointer old) => old._offset != _offset;
}

When we run the application we should get the output of the screen below like video below the screen. If you try to move the pointer around the screen, the application will lag terribly as it redraws the background, requiring expensive calculations.

Next, we will add RepaintBoundary. The answer to the above problem is to wrap the CustomPaint widget as a child Widget of RepaintBoundary.

Widget _buildBackground() {
  return RepaintBoundary(
    child: CustomPaint(
      painter: BackgroundColor(MediaQuery.of(context).size),
      isComplex: true,
      willChange: false,
    ),
  );
}

When we run the application we should get the output of the screen like the video below the screen. With this simple change, now when Flutter redraws the cursor, the background does not need to be redrawn. The application should no longer lag.

The entire code is shown below.

import 'package:flutter/material.dart';
import 'package:flutter_repaint_boundary_demo/background_color.dart';
import 'package:flutter_repaint_boundary_demo/cursor_pointer.dart';

class HomePage extends StatefulWidget {

  @override
  State createState() => new _HomePageState();
}

class _HomePageState extends State<HomePage> {

  final GlobalKey _paintKey = new GlobalKey();
  Offset _offset = Offset.zero;

  Widget _buildBackground() {
    return RepaintBoundary(
      child: CustomPaint(
        painter: BackgroundColor(MediaQuery.of(context).size),
        isComplex: true,
        willChange: false,
      ),
    );
  }

  Widget _buildCursor() {
    return Listener(
      onPointerDown: _updateOffset,
      onPointerMove: _updateOffset,
      child: CustomPaint(
        key: _paintKey,
        painter: CursorPointer(_offset),
        child: ConstrainedBox(
          constraints: BoxConstraints.expand(),
        ),
      ),
    );
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        automaticallyImplyLeading: false,
        backgroundColor: Colors.cyan,
        title: const Text('Flutter RepaintBoundary Demo'),
      ),
      body: Stack(
        fit: StackFit.expand,
        children: <Widget>[
          _buildBackground(),
          _buildCursor(),
        ],
      ),
    );
  }

  _updateOffset(PointerEvent event) {
    RenderBox? referenceBox = _paintKey.currentContext?.findRenderObject() as RenderBox;
    Offset offset = referenceBox.globalToLocal(event.position);
    setState(() {
      _offset = offset;
    });
  }
}

Summary

In the article, I explained the basic structure of RepaintBoundary in Flutter; you can modify this code according to your choice. This is my small introduction to RepaintBoundary On User Interaction, which is possible when using Flutter.

Android study notes

Android performance optimization: https://qr18.cn/FVlo89
Android car version: https://qr18.cn/F05ZCM
Android reverse security study notes: https://qr18.cn/CQ5TcL
The underlying principles of Android Framework: https://qr18.cn/AQpN4J
Android audio and video: https://qr18.cn/Ei3VPD
Jetpack family bucket chapter (including Compose): https://qr18.cn/A0gajp
Kotlin article: https://qr18.cn/CdjtAF
Gradle article: https://qr18.cn/DzrmMB
OkHttp source code analysis notes: https://qr18.cn/Cw0pBD
Flutter article: https://qr18.cn/DIvKma
Eight major bodies of Android knowledge: https://qr18.cn/CyxarU
Android core notes: https://qr21.cn/CaZQLo
Android interview questions from previous years: https://qr18.cn/CKV8OZ
The latest Android interview question set in 2023: https://qr18.cn/CgxrRy
Interview questions for Android vehicle development positions: https://qr18.cn/FTlyCJ
Audio and video interview questions: https://qr18.cn/AcV6Ap