Comment puis-je afficher un dialogue la toute première fois que je charge une page ? Comment puis-je faire quelque chose une fois que la construction est terminée ?

Question

J’ai reçu à plusieurs reprises des questions telles que

  • Comment puis-je afficher un dialogue uniquement la première fois qu’une page est affichée ?
  • Comment puis-je obtenir les dimensions de quelque chose qui dépend des dimensions de l’appareil et d’autres contraintes ?
  • Comment puis-je faire quelque chose une fois la construction terminée ?

Réponse

Toutes ces questions ont un point commun, il faut attendre la fin de build avant de faire quelque chose…

Prenons chacune des questions, une par une, pour expliquer comment Flutter fonctionne…


Comment puis-je afficher un dialogue uniquement la première fois qu’une page est affichée ?

Lorsque vous commencez dans Flutter et que vous avez besoin d’afficher quelque chose à l'écran au chargement de la page, il est très naturel de penser que vous pouvez le déclencher depuis le initState() ou même depuis le Widget build(BuildContext context)

Vous vous retrouvez alors avec un code qui peut ressembler à l’un des suivants :

  ...
  @override
  void initState(){
    super.initState();
    showDialog(
      ...
    );
  }

ou

  ...
  @override
  Widget build(BuildContext context){
    showDialog(
      ...
    );
    return Scaffold(
      ...
    );
  }

Je pense que tous ceux qui ont commencé avec Flutter ont pensé un jour que c'était la bonne façon de procéder… mais vous avez systématiquement reçu un message d’erreur de la console de débogage indiquant quelque chose comme

  • setState() or markNeedsBuild() called during build. This Overlay widget cannot be marked as needing to build because the framework is already in the process of building widgets…
  • inheritFromWidgetOfExactType(_LocalizationsScope) or inheritFromElement() was called before _PageState.initState() completed…

Qu’est-ce que cela signifie et pourquoi je ne peux pas le faire de cette façon ?

Pour le comprendre, il faut revenir à l’essentiel et plus précisément sur le “StatefulWidget Lifecyle” (voir mon article Widget - State - Context - InheritedWidget).

Lorsque vous chargez une Page (= “Route"), cette dernière est soit un StatelessWidget, soit un StatefulWidget, mais si vous considérez la méthode initState(), il s’agit d’un StatefulWidget. Donc, continuons l’explication avec un StatefulWidget.

Ainsi, lorsque vous demandez à Flutter d’afficher une nouvelle Page (=Route), en coulisse, Flutter doit rendre votre page sur l'écran de votre appareil. Cela se fait au cours d’un processus appelé “Rendu d’image". (Je vous expliquerai tout cela dans un prochain article).

Lors de ce Rendu d’image, lié à la première fois que nous voulons afficher la nouvelle page, le Widget est gonflé. Cela signifie qu’il crée un Element (= BuildContext) qui démarre son cycle de vie et appelle les méthodes suivantes en séquence :

  • initState
  • didChangeDependencies
  • build

C’est SEULEMENT une fois que la méthode build est terminée, que quelques millisecondes plus tard le frame rendering de cette page est complet et que vous pouvez demander d’afficher autre chose, en plus.

Que se passe-t-il lorsque j’appelle showDialog() dans le initState()?

En fait, lorsque Flutter exécute la méthode initState(), il se trouve au milieu d’un processus de rendu d’image.

Lorsque vous invoquez la méthode showDialog(), vous demandez à Flutter d’essayer de trouver le Widget Overlay le plus proche dans l’arbre des Widgets, en utilisant le context comme source de recherche…

Gardez à l’esprit que le context est, en fait, l’ Element qui correspond au Widget que vous êtes en train de gonfler et que lors de l’appel au initState(), ce élément n’est pas encore monté (= pas encore formellement positionné dans l’arbre). Par conséquent, ce context ne peut pas encore être utilisé pour rechercher le Overlay la plus proche, car pas encore formellement dans l’arbre…

Que se passe-t-il lorsque j’appelle showDialog() de la méthode build(…) ?

Lorsque vous invoquez le showDialog() de la méthode build(BuildContext), Flutter trouve le widget Overlay le plus proche et lui demande d’insérer un nouveau OverlayEntry, qui sera utilisé comme conteneur pour le dialogue que vous voulez afficher.

Comme le Overlay a besoin d'être rafraîchi (pour afficher le dialog), il demande au Flutter Framework de le reconstruire mais… Flutter est *déjà dans un processus de construction. Par conséquent, Flutter rejette la requête et lance une exception.

Alors, quelle est la solution ?

Flutter Framework a une API pratique pour demander l’exécution d’une méthode de rappel une fois qu’un rendu d’image est terminé. Cette méthode est :

WidgetsBinding.instance.addPostFrameCallback.


Comment utiliser le addPostFrameCallback ?

Cas 1 : Afficher un dialogue seulement la première fois que vous affichez une page

...
@override
void initState(){
  super.initState();
  WidgetsBinding.instance.addPostFrameCallback((_){
    showDialog(
      context: context, 
      ...
    );
  });
}

Comme le initState n’est invoqué que la toute première fois où vous gonflez le StatefulWidget, cette méthode ne sera jamais appelée une deuxième fois.

Par conséquent, vous pouvez demander au addPostFrameCallback d’afficher votre dialogue à partir de cette méthode. Le showDialog sera exécuté après que la construction soit terminée.

Cas 2 : Faites quelque chose une fois que la construction est complète

Il peut être très utile d’attendre que le rendu soit complet pour, par exemple, obtenir les dimensions exactes du Widget. Pour cela, il faut

@override
Widget build(BuildContext context){
  WidgetsBinding.instance.addPostFrameCallback((_) => onAfterBuild(context));

  return Container(
    ...
  );
}

void _onAfterBuild(BuildContext context){
  // Je peux maintenant obtenir les dimensions en toute sécurité en fonction du contexte
}

Conclusion

Il existe de nombreux cas où il est très pratique d’utiliser l’API addPostFrameCallback et vous avez peut-être déjà remarqué que je l’ai utilisée dans certains de mes articles…

J’espère vraiment que vous trouverez cette explication utile.