Améliorer la reconstruction de son app avec RepaintBoundary


Avatar de Pierre Courtois

Vous souhaitez rendre votre application plus fluide ? Dans ce guide rapide, je vous explique comment réaliser cela avec le widget RepaintBoundary.


Pourquoi RepaintBoundary est important ?

Être capable d’offrir une expérience fluide est un point très important lorsque l’on code une application Flutter. C’est particulièrement le cas des animations qui vont demander des mises à jour fréquentes de l’interface. En effet, à chaque fois qu’un widget est redessiné (repaint), Flutter doit recalculer une partie ou la totalité de l’arbre de rendu, ce qui peut vite être coûteux en ressources, surtout sur des appareils moins puissants.

C’est là qu’intervient RepaintBoundary. Ce widget permet d’isoler une sous-arborescence de widgets pour limiter les zones redessinées lors des mises à jour. En d’autres termes, il agit comme une « frontière » qui indique à Flutter : « Ne redessine que ce qui est à l’intérieur de cette frontière si rien d’autre ne change autour ». Cela réduit ainsi les calculs inutiles et améliore les performances de l’application.

Comment utiliser RepaintBoundary

Ce widget ne montre rien visuellement dans l’application, mais vient envelopper le widget enfant que vous allez reconstruire fréquemment. Voici quelques cas d’utilisation courants :

  • Animations locales : Une icône qui clignote ou une barre de progression.
  • Listes avec mises à jour partielles : Un item qui change sans affecter les autres.
  • Effets isolés : Un widget qui se redessine fréquemment sans dépendre de son parent.

Voici comment le mettre en place :

Commencez par activer debugRepaintRainbowEnabled dans votre fonction main(). Cette propriété va dessiner une bordure autour de tous les éléments de votre application, qui change de couleur à chaque fois que l’un d’eux se reconstruit.

Puis voici un exemple où j’utilise RepaintBoundary pour limiter la reconstruction de mon écran, au widget qui est modifié.

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

void main() {
  debugRepaintRainbowEnabled = true; // Active les bordures arc-en-ciel
  runApp(MyApp());
}

class MyApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      home: Scaffold(
        backgroundColor: Colors.black, // Fond sombre pour contraste
        appBar: AppBar(title: Text("RepaintBoundary Demo")),
        body: Center(
          child: Column(
            mainAxisAlignment: MainAxisAlignment.center,
            children: [
              Text(
                "Texte statique au-dessus",
                style: TextStyle(fontSize: 20, color: Colors.white),
              ),
              SizedBox(height: 20),
              Container(
                width: 200,
                height: 200,
                color: Colors.grey[800], // Fond gris foncé
                child: Center(
                  // Avec RepaintBoundary autour de l'animation
                  child: RepaintBoundary(
                    child: RotatingSquare(),
                  ),
                ),
              ),
              SizedBox(height: 20),
              Text(
                "Texte statique en-dessous",
                style: TextStyle(fontSize: 20, color: Colors.white),
              ),
            ],
          ),
        ),
      ),
    );
  }
}

class RotatingSquare extends StatefulWidget {
  @override
  _RotatingSquareState createState() => _RotatingSquareState();
}

class _RotatingSquareState extends State<RotatingSquare>
    with SingleTickerProviderStateMixin {
  late AnimationController _controller;
  late Animation<double> _rotationAnimation;

  @override
  void initState() {
    super.initState();
    _controller = AnimationController(
      duration: Duration(seconds: 1),
      vsync: this,
    )..repeat();

    _rotationAnimation = Tween<double>(begin: 0, end: 2 * 3.14159).animate(_controller);
  }

  @override
  void dispose() {
    _controller.dispose();
    super.dispose();
  }

  @override
  Widget build(BuildContext context) {
    return AnimatedBuilder(
      animation: _controller,
      builder: (context, child) {
        return Transform.rotate(
          angle: _rotationAnimation.value,
          child: Container(
            width: 50,
            height: 50,
            color: Colors.red, // Couleur vive pour visibilité
          ),
        );
      },
    );
  }
}

Grâce à la bordure en arc en ciel, on peut voir ici que le carré rouge se reconstruit à chaque frame, mais pas le reste de l’application qui est statique.

Une deuxième manière de s’assurer que l’application gagne en performances est d’activer debugProfilePaintsEnabled = true; et de regarder ce qui ressort dans votre console de débogage.

Les limites de RepaintBoundary

Au vu de ses avantages, on pourrait être vite tenté d’envelopper tous ses widgets dans des RepaintBoundary, mais ce n’est pas conseillé. En effet, comme toute solution, elle vient aussi avec son lot de défaut :

  1. Ajouter des RepaintBoundary consomme plus de mémoire, puisque vous ajoutez toute une nouvelle couche de widget en plus. À trop l’utilisez, vous risquez donc de rendre votre application moins performante.
  2. Si toute la page doit être reconstruite (par exemple sur une animation qui prend tout l’écran), il n’y a alors pas d’intérêt à l’utiliser.
  3. Si votre widget dépend d’un état qui change à l’extérieur, alors tout l’écran va se reconstruire malgré le RepaintBoundary.

Ce widget doit donc être utilisé de manière intelligente, mais peut améliorer les performances de votre application, lorsque utilisé au bon endroit.

Avatar de Pierre Courtois