StatefulWidget : comprendre et optimiser les mises à jour visuelles dans Flutter

19/06/2023

0min

StatefulWidget : comprendre et optimiser les mises à jour visuelles dans Flutter

19/06/2023

0min

Dans un précédent article, nous avons fait nos premiers pas avec Flutter et ses Widgets. Nous avons vu les StatelessWidget, et les StatefulWidget.

class MaCollectionDeWidgets extends StatefulWidget {
 @override
 State<MaCollectionDeWidgets> createState() => MaCollectionDeWidgetsState();
}

class MaCollectionDeWidgetsState extends State<MaCollectionDeWidgets> {
 late List<MonWidget> list;

 @override
 void initState() {
   list = [
     MonWidget(),
     MonWidget(),
   ];
   super.initState();
 }

 @override
 Widget build(BuildContext context) {
   return Scaffold(
     body: Column(
       children: list,
     ),
     floatingActionButton: FloatingActionButton(onPressed: _permutate),
   );
 }

 _permutate() {
   setState(() {
     var w = list.removeAt(0);
     list.add(w);
   });
 }
}

>Les StatefulWidget, qu’est ce que c’est ?,

class MaCollectionDeWidgets extends StatefulWidget {
 @override
 State<MaCollectionDeWidgets> createState() => MaCollectionDeWidgetsState();
}

class MaCollectionDeWidgetsState extends State<MaCollectionDeWidgets> {
 late List<MonWidget> list;

 @override
 void initState() {
   list = [
     MonWidget(),
     MonWidget(),
   ];
   super.initState();
 }

 @override
 Widget build(BuildContext context) {
   return Scaffold(
     body: Column(
       children: list,
     ),
     floatingActionButton: FloatingActionButton(onPressed: _permutate),
   );
 }

 _permutate() {
   setState(() {
     var w = list.removeAt(0);
     list.add(w);
   });
 }
}

>

Les StatefulWidget jouent un rôle essentiel dans le développement d’applications Flutter en permettant la gestion des états dynamiques, et méritent un article complet sur leur fonctionnement. Aujourd’hui, nous allons explorer en détail la théorie derrière les StatefulWidget : le fonctionnement de l’event loop, des différents arbres (widget, render et visual tree), et ce qu’il se passe lorsque la méthode setState est appelée. De plus, nous aborderons les bonnes pratiques pour optimiser les mises à jour des widgets.

,

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

void main() {
 runApp(MaterialApp(home: MaCollectionDeWidgets()));
}

>L’Event Loop,

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

void main() {
 runApp(MaterialApp(home: MaCollectionDeWidgets()));
}

>

L’event loop est un mécanisme qui permet de gérer les événements et les tâches asynchrones de manière ordonnée. Il s’agit d’une boucle infinie qui attend constamment de nouveaux événements et tâches à traiter. Dans le contexte de Flutter, l’event loop est responsable de la mise à jour de l’interface utilisateur en réponse à des événements tels que les interactions de l’utilisateur, les requêtes réseau, les mises à jour d’état, etc.

Une application Dart possède une seule event loop, avec deux queues : l’event queue, et la microtask queue.

L’event queue contient tous les événements externes (les I/O, la souris qui bouge, l’appuie sur une touche du clavier, etc) mais aussi tous les événements relatifs à Dart (méthodes asynchrones avec retour différé, timers, etc…).

La microtask queue quant à elle est uniquement utilisée pour les événements Dart.

Elle est utile dans le sens où le code de gestion des événements doit parfois effectuer une tâche ultérieurement, mais avant de rendre le contrôle à l’event loop. Par exemple, lorsqu’un objet visuel change, il regroupe plusieurs sous-modifications et les signale de manière asynchrone.

La microtask queue permet à l’objet observable de signaler ces modifications avant que le DOM puisse afficher cet état incohérent, car la microtask queue doit être vide avant de passer au prochain événement de l’event queue.

,

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

void main() {
 runApp(MaterialApp(home: MaCollectionDeWidgets()));
}

>Les arbres,

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

void main() {
 runApp(MaterialApp(home: MaCollectionDeWidgets()));
}

>

Un bon informaticien est un bon arboriste. Cette structure de données que nous chérissons tant se retrouve bien évidemment dans Flutter. On distingue


,

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

void main() {
 runApp(MaterialApp(home: MaCollectionDeWidgets()));
}

types d’arbres :

Le widget tree est une structure hiérarchique composée de widgets. Les widgets sont des objets immuables qui décrivent comment l’interface utilisateur doit être rendue. Chaque widget a des propriétés ou attributs qui définissent son apparence et son comportement. Les widgets peuvent être imbriqués les uns dans les autres pour créer des interfaces utilisateur complexes. Le widget tree représente la structure de votre interface utilisateur et définit les relations parent-enfant entre les widgets.

L’element tree est une représentation intermédiaire entre le widget tree et le render tree. Chaque widget du widget tree est associé à un élément correspondant dans l’element tree. Les éléments sont des objets mutable utilisés par le framework pour gérer les mises à jour et les réconciliations lors des reconstructions de l’interface utilisateur. L’element tree reflète l’état actuel des widgets et fournit des informations supplémentaires nécessaires à la mise à jour efficace de l’interface utilisateur.

Enfin, le render tree est la représentation finale de l’interface utilisateur à afficher sur l’écran. Chaque élément de l’element tree est associé à un objet RenderObject correspondant dans le render tree. Les RenderObject sont des objets qui savent comment se dessiner à l’écran. Le render tree contient les informations nécessaires pour effectuer le rendu réel des widgets à l’écran, en tenant compte de la position, de la taille, de l’opacité, de la transformation, etc. des widgets.

Flutter utilise ces trois arbres pour construire et rendre l’interface graphique de manière efficace.

Lorsque des changements sont effectués dans le widget tree, Flutter reconstruit l’element tree en comparant les nouveaux widgets avec les anciens. Cela permet d’identifier les parties de l’interface utilisateur qui ont besoin d’être mises à jour.

Ensuite, Flutter met à jour le render tree en appliquant les changements nécessaires pour refléter l’état actuel des widgets.

Enfin, le render tree est utilisé pour générer les instructions de rendu qui sont envoyées à la plate-forme sous-jacente (par exemple, Android ou iOS) pour présenter le résultat final sur l’écran de l’utilisateur.

Ce processus de mise à jour de l’interface utilisateur est rapide et efficace grâce à l’utilisation de l’element tree intermédiaire, qui permet d’éviter de recalculer tout le render tree à chaque modification.

,

class MaCollectionDeWidgets extends StatefulWidget {
 @override
 State<MaCollectionDeWidgets> createState() => MaCollectionDeWidgetsState();
}

class MaCollectionDeWidgetsState extends State<MaCollectionDeWidgets> {
 late List<MonWidget> list;

 @override
 void initState() {
   list = [
     MonWidget(),
     MonWidget(),
   ];
   super.initState();
 }

 @override
 Widget build(BuildContext context) {
   return Scaffold(
     body: Column(
       children: list,
     ),
     floatingActionButton: FloatingActionButton(onPressed: _permutate),
   );
 }

 _permutate() {
   setState(() {
     var w = list.removeAt(0);
     list.add(w);
   });
 }
}

>Et nos StatefulWidgets dans tout ça ?,

class MaCollectionDeWidgets extends StatefulWidget {
 @override
 State<MaCollectionDeWidgets> createState() => MaCollectionDeWidgetsState();
}

class MaCollectionDeWidgetsState extends State<MaCollectionDeWidgets> {
 late List<MonWidget> list;

 @override
 void initState() {
   list = [
     MonWidget(),
     MonWidget(),
   ];
   super.initState();
 }

 @override
 Widget build(BuildContext context) {
   return Scaffold(
     body: Column(
       children: list,
     ),
     floatingActionButton: FloatingActionButton(onPressed: _permutate),
   );
 }

 _permutate() {
   setState(() {
     var w = list.removeAt(0);
     list.add(w);
   });
 }
}

>

Les StatefulWidget sont des objets immuables possédant un état. Cet état est stocké and une classe séparée qui est instancié par la méthode createState :

class MonWidget extends StatefulWidget { int counter = 0; MonWidget({super.key}); @override State<MonWidget> createState() => MonWidgetState(); } class MonWidgetState extends State<MonWidget> { @override void initState() { // Tous les trucs et les bidules à exécuter // avant que la méthode build soit appelée super.initState(); } @override Widget build(BuildContext context) { return GestureDetector( onTap: () { setState(() { widget.counter++; }); }, child: Container( color: Color(Random().nextInt(0xffffffff)), child: SizedBox( width: 200, height: 200, child: Text("${widget.counter}"), ), ), ); } }.

,

class MaCollectionDeWidgets extends StatefulWidget {
 @override
 State<MaCollectionDeWidgets> createState() => MaCollectionDeWidgetsState();
}

class MaCollectionDeWidgetsState extends State<MaCollectionDeWidgets> {
 late List<MonWidget> list;

 @override
 void initState() {
   list = [
     MonWidget(),
     MonWidget(),
   ];
   super.initState();
 }

 @override
 Widget build(BuildContext context) {
   return Scaffold(
     body: Column(
       children: list,
     ),
     floatingActionButton: FloatingActionButton(onPressed: _permutate),
   );
 }

 _permutate() {
   setState(() {
     var w = list.removeAt(0);
     list.add(w);
   });
 }
}

5rem; --cbp-tab-width:


,

class MaCollectionDeWidgets extends StatefulWidget {
 @override
 State<MaCollectionDeWidgets> createState() => MaCollectionDeWidgetsState();
}

class MaCollectionDeWidgetsState extends State<MaCollectionDeWidgets> {
 late List<MonWidget> list;

 @override
 void initState() {
   list = [
     MonWidget(),
     MonWidget(),
   ];
   super.initState();
 }

 @override
 Widget build(BuildContext context) {
   return Scaffold(
     body: Column(
       children: list,
     ),
     floatingActionButton: FloatingActionButton(onPressed: _permutate),
   );
 }

 _permutate() {
   setState(() {
     var w = list.removeAt(0);
     list.add(w);
   });
 }
}

;" data-code-block-pro-font-family="Code-Pro-JetBrains-Mono">


,

class MonWidget extends StatefulWidget {
 const MonWidget({super.key});

 @override
 State<MonWidget> createState() => MonWidgetState();
}

class MonWidgetState extends State<MonWidget> {

 @override
 void initState() {
   // Tous les trucs et les bidules à exécuter
   // avant que la méthode build soit appelée
   super.initState();
 }

 @override
 Widget build(BuildContext context) {
   // Votre widget
   throw UnimplementedError();
 }
}

>

Lorsqu'un StatefulWidget est utilisé, son état interne est stocké dans l’element tree et géré séparément du render tree. Cela signifie que son état peut être modifié sans reconstruire tout l'arbre de rendu. Mais ce n’est pas aussi simple que ça. Notez ici l’utilisation de l’attribut clé dans le constructeur de MonWidget. Une clé est un élément unique permettant d’identifier notre objet dans l’arbre. Supposons une application simple : deux carrés, dans une colonne de couleur aléatoire à l’instantiation, et un bouton permuttant les carrés.

class MonWidget extends StatefulWidget { int counter = 0; MonWidget({super.key}); @override State<MonWidget> createState() => MonWidgetState(); } class MonWidgetState extends State<MonWidget> { @override void initState() { // Tous les trucs et les bidules à exécuter // avant que la méthode build soit appelée super.initState(); } @override Widget build(BuildContext context) { return GestureDetector( onTap: () { setState(() { widget.counter++; }); }, child: Container( color: Color(Random().nextInt(0xffffffff)), child: SizedBox( width: 200, height: 200, child: Text("${widget.counter}"), ), ), ); } }.

,

class MaCollectionDeWidgets extends StatefulWidget {
 @override
 State<MaCollectionDeWidgets> createState() => MaCollectionDeWidgetsState();
}

class MaCollectionDeWidgetsState extends State<MaCollectionDeWidgets> {
 late List<MonWidget> list;

 @override
 void initState() {
   list = [
     MonWidget(),
     MonWidget(),
   ];
   super.initState();
 }

 @override
 Widget build(BuildContext context) {
   return Scaffold(
     body: Column(
       children: list,
     ),
     floatingActionButton: FloatingActionButton(onPressed: _permutate),
   );
 }

 _permutate() {
   setState(() {
     var w = list.removeAt(0);
     list.add(w);
   });
 }
}

5rem; --cbp-tab-width:


,

class MaCollectionDeWidgets extends StatefulWidget {
 @override
 State<MaCollectionDeWidgets> createState() => MaCollectionDeWidgetsState();
}

class MaCollectionDeWidgetsState extends State<MaCollectionDeWidgets> {
 late List<MonWidget> list;

 @override
 void initState() {
   list = [
     MonWidget(),
     MonWidget(),
   ];
   super.initState();
 }

 @override
 Widget build(BuildContext context) {
   return Scaffold(
     body: Column(
       children: list,
     ),
     floatingActionButton: FloatingActionButton(onPressed: _permutate),
   );
 }

 _permutate() {
   setState(() {
     var w = list.removeAt(0);
     list.add(w);
   });
 }
}

;" data-code-block-pro-font-family="Code-Pro-JetBrains-Mono">


,

class MonWidget extends StatefulWidget {
 int counter = 0;

 MonWidget({super.key});

 @override
 State<MonWidget> createState() => MonWidgetState();
}

class MonWidgetState extends State<MonWidget> {
 @override
 void initState() {
   // Tous les trucs et les bidules à exécuter
   // avant que la méthode build soit appelée
   super.initState();
 }

 @override
 Widget build(BuildContext context) {
   return GestureDetector(
     onTap: () {
       setState(() {
         widget.counter++;
       });
     },
     child: Container(
       color: Color(Random().nextInt(0xffffffff)),
       child: SizedBox(
         width: 200,
         height: 200,
         child: Text("${widget.counter}"),
       ),
     ),
   );
 }
}

>

Cette classe représentera notre carré de couleur aléatoire

class MonWidget extends StatefulWidget { int counter = 0; MonWidget({super.key}); @override State<MonWidget> createState() => MonWidgetState(); } class MonWidgetState extends State<MonWidget> { @override void initState() { // Tous les trucs et les bidules à exécuter // avant que la méthode build soit appelée super.initState(); } @override Widget build(BuildContext context) { return GestureDetector( onTap: () { setState(() { widget.counter++; }); }, child: Container( color: Color(Random().nextInt(0xffffffff)), child: SizedBox( width: 200, height: 200, child: Text("${widget.counter}"), ), ), ); } }.

,

class MaCollectionDeWidgets extends StatefulWidget {
 @override
 State<MaCollectionDeWidgets> createState() => MaCollectionDeWidgetsState();
}

class MaCollectionDeWidgetsState extends State<MaCollectionDeWidgets> {
 late List<MonWidget> list;

 @override
 void initState() {
   list = [
     MonWidget(),
     MonWidget(),
   ];
   super.initState();
 }

 @override
 Widget build(BuildContext context) {
   return Scaffold(
     body: Column(
       children: list,
     ),
     floatingActionButton: FloatingActionButton(onPressed: _permutate),
   );
 }

 _permutate() {
   setState(() {
     var w = list.removeAt(0);
     list.add(w);
   });
 }
}

5rem; --cbp-tab-width:


,

class MaCollectionDeWidgets extends StatefulWidget {
 @override
 State<MaCollectionDeWidgets> createState() => MaCollectionDeWidgetsState();
}

class MaCollectionDeWidgetsState extends State<MaCollectionDeWidgets> {
 late List<MonWidget> list;

 @override
 void initState() {
   list = [
     MonWidget(),
     MonWidget(),
   ];
   super.initState();
 }

 @override
 Widget build(BuildContext context) {
   return Scaffold(
     body: Column(
       children: list,
     ),
     floatingActionButton: FloatingActionButton(onPressed: _permutate),
   );
 }

 _permutate() {
   setState(() {
     var w = list.removeAt(0);
     list.add(w);
   });
 }
}

;" data-code-block-pro-font-family="Code-Pro-JetBrains-Mono">


,

class MaCollectionDeWidgets extends StatefulWidget {
 @override
 State<MaCollectionDeWidgets> createState() => MaCollectionDeWidgetsState();
}

class MaCollectionDeWidgetsState extends State<MaCollectionDeWidgets> {
 late List<MonWidget> list;

 @override
 void initState() {
   list = [
     MonWidget(),
     MonWidget(),
   ];
   super.initState();
 }

 @override
 Widget build(BuildContext context) {
   return Scaffold(
     body: Column(
       children: list,
     ),
     floatingActionButton: FloatingActionButton(onPressed: _permutate),
   );
 }

 _permutate() {
   setState(() {
     var w = list.removeAt(0);
     list.add(w);
   });
 }
}

>

class MonWidget extends StatefulWidget { int counter = 0; MonWidget({super.key}); @override State<MonWidget> createState() => MonWidgetState(); } class MonWidgetState extends State<MonWidget> { @override void initState() { // Tous les trucs et les bidules à exécuter // avant que la méthode build soit appelée super.initState(); } @override Widget build(BuildContext context) { return GestureDetector( onTap: () { setState(() { widget.counter++; }); }, child: Container( color: Color(Random().nextInt(0xffffffff)), child: SizedBox( width: 200, height: 200, child: Text("${widget.counter}"), ), ), ); } }


,

class MaCollectionDeWidgets extends StatefulWidget {
 @override
 State<MaCollectionDeWidgets> createState() => MaCollectionDeWidgetsState();
}

class MaCollectionDeWidgetsState extends State<MaCollectionDeWidgets> {
 late List<MonWidget> list;

 @override
 void initState() {
   list = [
     MonWidget(),
     MonWidget(),
   ];
   super.initState();
 }

 @override
 Widget build(BuildContext context) {
   return Scaffold(
     body: Column(
       children: list,
     ),
     floatingActionButton: FloatingActionButton(onPressed: _permutate),
   );
 }

 _permutate() {
   setState(() {
     var w = list.removeAt(0);
     list.add(w);
   });
 }
}

.


,

class MonWidget extends StatefulWidget {
 const MonWidget({super.key});

 @override
 State<MonWidget> createState() => MonWidgetState();
}

class MonWidgetState extends State<MonWidget> {

 @override
 void initState() {
   // Tous les trucs et les bidules à exécuter
   // avant que la méthode build soit appelée
   super.initState();
 }

 @override
 Widget build(BuildContext context) {
   // Votre widget
   throw UnimplementedError();
 }
}

pt; line-height:


,

class MonWidget extends StatefulWidget {
 int counter = 0;

 MonWidget({super.key});

 @override
 State<MonWidget> createState() => MonWidgetState();
}

class MonWidgetState extends State<MonWidget> {
 @override
 void initState() {
   // Tous les trucs et les bidules à exécuter
   // avant que la méthode build soit appelée
   super.initState();
 }

 @override
 Widget build(BuildContext context) {
   return GestureDetector(
     onTap: () {
       setState(() {
         widget.counter++;
       });
     },
     child: Container(
       color: Color(Random().nextInt(0xffffffff)),
       child: SizedBox(
         width: 200,
         height: 200,
         child: Text("${widget.counter}"),
       ),
     ),
   );
 }
}

,

class MonWidget extends StatefulWidget {
 int counter = 0;

 MonWidget({super.key});

 @override
 State<MonWidget> createState() => MonWidgetState();
}

class MonWidgetState extends State<MonWidget> {
 @override
 void initState() {
   // Tous les trucs et les bidules à exécuter
   // avant que la méthode build soit appelée
   super.initState();
 }

 @override
 Widget build(BuildContext context) {
   return GestureDetector(
     onTap: () {
       setState(() {
         widget.counter++;
       });
     },
     child: Container(
       color: Color(Random().nextInt(0xffffffff)),
       child: SizedBox(
         width: 200,
         height: 200,
         child: Text("${widget.counter}"),
       ),
     ),
   );
 }
}

5%;">Cette classe représente notre liste de carrés avec le bouton pour permuter les carrés


,

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

void main() {
 runApp(MaterialApp(home: MaCollectionDeWidgets()));
}

>

Et notre entry point avec l’import des librairies

En lançant l’application et en cliquant sur le bouton pour permuter les carrés, voici ce qui se produit entre le premier clic, et le second :

La couleur du carré change ! Pourtant, seulement deux instances sont créées dans MaCollectionDeWidgets.initState, et cette méthode n’est appelée qu’une seule fois ! La liste ainsi que les objets contenus dans cette liste ne sont pas censés changer. Et en effet : les instances de MonWidget sont les mêmes.

C’est parce que nos classes MonWidget ne possèdent pas de clés. En effet, en réorganisant la liste, et en appelant setState, notre arbre a changé. Flutter ne sachant pas identifier nos objets sans clé, il va seulement se rendre compte que le widget présent à cet endroit de l’arbre n’est plus ici, mais qu’un autre est présent, et qu’il doit le construire. Il appelle donc la méthode MonWidget.createState, sans recréer d’instance de MonWidget, rappelant notre constructeur avec sa nouvelle couleur.

Ajoutons une clé de cette manière :

class MonWidget extends StatefulWidget { int counter = 0; MonWidget({super.key}); @override State<MonWidget> createState() => MonWidgetState(); } class MonWidgetState extends State<MonWidget> { @override void initState() { // Tous les trucs et les bidules à exécuter // avant que la méthode build soit appelée super.initState(); } @override Widget build(BuildContext context) { return GestureDetector( onTap: () { setState(() { widget.counter++; }); }, child: Container( color: Color(Random().nextInt(0xffffffff)), child: SizedBox( width: 200, height: 200, child: Text("${widget.counter}"), ), ), ); } }.

,

class MaCollectionDeWidgets extends StatefulWidget {
 @override
 State<MaCollectionDeWidgets> createState() => MaCollectionDeWidgetsState();
}

class MaCollectionDeWidgetsState extends State<MaCollectionDeWidgets> {
 late List<MonWidget> list;

 @override
 void initState() {
   list = [
     MonWidget(),
     MonWidget(),
   ];
   super.initState();
 }

 @override
 Widget build(BuildContext context) {
   return Scaffold(
     body: Column(
       children: list,
     ),
     floatingActionButton: FloatingActionButton(onPressed: _permutate),
   );
 }

 _permutate() {
   setState(() {
     var w = list.removeAt(0);
     list.add(w);
   });
 }
}

5rem; --cbp-tab-width:


,

class MaCollectionDeWidgets extends StatefulWidget {
 @override
 State<MaCollectionDeWidgets> createState() => MaCollectionDeWidgetsState();
}

class MaCollectionDeWidgetsState extends State<MaCollectionDeWidgets> {
 late List<MonWidget> list;

 @override
 void initState() {
   list = [
     MonWidget(),
     MonWidget(),
   ];
   super.initState();
 }

 @override
 Widget build(BuildContext context) {
   return Scaffold(
     body: Column(
       children: list,
     ),
     floatingActionButton: FloatingActionButton(onPressed: _permutate),
   );
 }

 _permutate() {
   setState(() {
     var w = list.removeAt(0);
     list.add(w);
   });
 }
}

;" data-code-block-pro-font-family="Code-Pro-JetBrains-Mono">


,

@override
 void initState() {
   list = [
     MonWidget(
       key: Key("Numerobis"),
     ),
     MonWidget(
       key: Key("Numeroter"),
     ),
   ];
   super.initState();
 }

>

Et observons le résultat :

Ouf, j’ai bien cru que je devenais fou :’)

Si nous avions une liste de widgets dont les propriétés ne changeaient pas, nous ne nous serions même pas rendu compte que nos widgets étaient complètement reconstruits !

,

class MaCollectionDeWidgets extends StatefulWidget {
 @override
 State<MaCollectionDeWidgets> createState() => MaCollectionDeWidgetsState();
}

class MaCollectionDeWidgetsState extends State<MaCollectionDeWidgets> {
 late List<MonWidget> list;

 @override
 void initState() {
   list = [
     MonWidget(),
     MonWidget(),
   ];
   super.initState();
 }

 @override
 Widget build(BuildContext context) {
   return Scaffold(
     body: Column(
       children: list,
     ),
     floatingActionButton: FloatingActionButton(onPressed: _permutate),
   );
 }

 _permutate() {
   setState(() {
     var w = list.removeAt(0);
     list.add(w);
   });
 }
}

>L’utilisation de setState,

class MaCollectionDeWidgets extends StatefulWidget {
 @override
 State<MaCollectionDeWidgets> createState() => MaCollectionDeWidgetsState();
}

class MaCollectionDeWidgetsState extends State<MaCollectionDeWidgets> {
 late List<MonWidget> list;

 @override
 void initState() {
   list = [
     MonWidget(),
     MonWidget(),
   ];
   super.initState();
 }

 @override
 Widget build(BuildContext context) {
   return Scaffold(
     body: Column(
       children: list,
     ),
     floatingActionButton: FloatingActionButton(onPressed: _permutate),
   );
 }

 _permutate() {
   setState(() {
     var w = list.removeAt(0);
     list.add(w);
   });
 }
}

>

La méthode setState est une méthode importante dans les StatefulWidget, inutile d’hériter de StatefulWidget si vous ne l’appelez pas 🙂 Elle indique à Flutter que l'état du widget a changé et que l'interface utilisateur doit être mise à jour en conséquence. Comme vu précédemment, setState(Function f) prend en argument une fonction contenant le code mettant à jour l’état de notre widget, puis s’exécutant de manière synchrone immédiatement après avoir appelé setState.

Lorsque setState est appelée, Flutter planifie la mise à jour du widget et ajoute une tâche à l'event loop pour effectuer cette mise à jour. Cependant, la mise à jour de l'interface utilisateur ne se produit pas immédiatement.

Au lieu de cela, Flutter effectue une réconciliation linéaire pour optimiser les performances. Cet algorithme, s’exécutant en temps linéaire, contribue grandement à rendre Flutter performant et permet de construire des interfaces utilisateur réactives et fluides.

L'approche générale de cet algorithme consiste à faire correspondre le début et la fin des deux listes d'enfants en comparant le type d'exécution et la clé (identifiant) de chaque widget, ce qui permet éventuellement de trouver une plage non vide au milieu de chaque liste contenant tous les enfants non correspondants. Ensuite, le framework place les enfants de la plage dans l'ancienne liste d'enfants dans une table de hachage basée sur leurs clés, puis parcourt la plage dans la nouvelle liste d'enfants et interroge la table de hachage par clé pour trouver des correspondances. Les enfants non correspondants sont supprimés et reconstruits à partir de zéro, tandis que les enfants correspondants sont reconstruits avec leurs nouveaux widgets.

Il est important de noter que les mises à jour de l'interface utilisateur dans Flutter sont généralement asynchrones. Cela signifie que la méthode build des widgets est appelée de manière différée, dans le cadre de l'event loop. Cela permet à Flutter de gérer efficacement les mises à jour d'interface utilisateur sans bloquer le thread principal, assurant ainsi une expérience utilisateur fluide même lors d'opérations intensives.

Quelques conseils

Nous venons de voir que lorsqu'un StatefulWidget est créé, il possède un état interne qui peut être modifié au fil du temps, et que la méthode setState est utilisée pour signaler à Flutter que l'état du widget a changé et que l'interface utilisateur doit être mise à jour en conséquence.

Cependant, il est important de noter que la méthode setState doit être utilisée de manière appropriée pour obtenir des mises à jour efficaces. Voici quelques bonnes pratiques à garder à l'esprit lors de l'utilisation de cette méthode :

  • Ne mettez à jour que les données nécessaires : Évitez de mettre à jour des données qui n'affectent pas l'interface utilisateur. Cela permet de limiter les reconstructions inutiles des widgets.
  • Évitez les appels redondants : Si plusieurs modifications d'état sont effectuées simultanément, vous pouvez regrouper ces modifications avant d'appeler setState une seule fois, et laisser le framework se débrouiller pour optimiser ses opérations entre l’arbre visuel et l’arbre de rendu.
  • Utilisez des variables immuables : Lorsque vous mettez à jour l'état d'un widget, utilisez des variables immuables autant que possible. Les variables immuables peuvent aider à éviter les effets de bord indésirables lors de la mise à jour de l'interface utilisateur.
  • Utilisez des widgets spécialisés pour les mises à jour spécifiques : Dans certains cas, vous pouvez utiliser des widgets spécialisés, tels que ValueNotifier et ValueListenableBuilder, pour gérer les mises à jour spécifiques d'une partie de l'interface utilisateur sans reconstruire tout le widget parent.
  • Utilisez les clés ! En particulier si vous utilisez des listes de widgets de la même classe, et que vous effectuez des opérations sur ces listes

Aussi, n’oubliez pas le Single Responsibility Principle des principes SOLID 😉 Non pas qu’il faille un widget par responsabilité, ce n’est que de la théorie, mais veillez à bien déléguer chaque responsabilité au bon widget. Chaque widget doit être responsable de son propre état et des modifications qui y sont apportées.

Enfin, voici les quelques conseils sur les optimisations graphiques que je peux vous donner :

  • Dans l’arbre des widgets, placez les Stateless en haut, et les Stateful en bas. Les StatefulWidget doivent être des feuilles se mettant elles-mêmes à jour
  • Limitez le nombre d’enfants directs : chaque méthode build doit instancier un minimum d’enfants. Il est préférable de bien séparer chaque élément graphique dans des classes séparées afin de réduire le nombre de classes instanciées explicitement dans cette méthode
  • Utilisez le plus de widgets constant que vous pouvez. Précisez bien le mot clé const avant l’appel au constructeur. La commande dart fix peut être utile pour modifier votre code et ajouter les const automatiquement
  • Quand vous le pouvez, mettez vos widgets en cache, et réutilisez le

Limitez les modifications de structure de votre arbre. Il vous sera parfoi nécessaire de rendre un type d’objet A avec des enfants, ou un type d’objet B avec les mêmes enfants, sous une certaine condition. Il est bien plus efficace d’utiliser un objet C ayant une propriété permettant réalisant le comportement attendu, que de reconstruire tout un sous-arbre. (Exemple : pensez à tout un pan de votre interface graphique qui détecte ou non les clics)

Conclusion

En conclusion, nous avons exploré le fonctionnement des StatefulWidget dans Flutter et comment optimiser les mises à jour visuelles.

Nous avons compris l'importance de l'event loop dans la gestion des événements et des tâches asynchrones, et avons examiné les trois types d'arbres utilisés par Flutter pour la construction et le rendu de l'interface utilisateur : le widget tree, l'element tree et le render tree.

Nous avons constaté que les StatefulWidget jouent un rôle crucial en permettant la gestion des états dynamiques dans les applications Flutter, et que grâce à la méthode setState, nous pouvons signaler à Flutter que l'état du widget a changé et que l'interface utilisateur doit être mise à jour en conséquence.

Nous avons aussi souligné quelques bonnes pratiques pour optimiser les mises à jour des widgets, telles que limiter les modifications d'état aux données nécessaires, regrouper les modifications d'état pour éviter les appels redondants, utiliser des variables immuables, et utiliser des widgets spécialisés lorsque cela est pertinent.

Enfin, nous avons abordé l'importance des clés (keys) pour identifier de manière unique les widgets dans l'arbre et éviter les reconstructions inutiles lors de réorganisations ou de modifications de listes de widgets.

En suivant ces bonnes pratiques et en comprenant les mécanismes sous-jacents des StatefulWidget, nous pouvons optimiser les mises à jour visuelles dans nos applications Flutter, offrant ainsi une expérience utilisateur réactive et fluide.

J’espère que cet article vous a été utile 🙂

Happy coding !

Nous avons lancé l'application CCIFI Connect pour avec Flutter si vous souhaitez voir le résultar

2025-02-19T11:03:23+01:00