Comment fonctionne réellement Flutter, en interne ?

Que sont Widgets, Elements, BuildContext, RenderOject, Bindings…

Difficulté: Débutant

Introduction

Lorsque j’ai commencé mon voyage dans le monde fabuleux de Flutter, on trouvait très peu de documentation sur Internet par rapport à ce qui existe aujourd’hui. Malgré le nombre d’articles qui ont été écrits, très peu parlent du fonctionnement réel de Flutter.

Que sont finalement les Widgets, les Elements, le BuildContext ? Pourquoi Flutter est-il rapide et pourquoi fonctionne-t-il parfois différemment de ce que l’on attend ? Que sont les arbres (= trees) ?

Lorsque vous écrivez une application, dans 95 % des cas, vous n’aurez affaire qu'à des Widgets pour afficher quelque chose ou interagir avec l'écran. Mais ne vous êtes-vous jamais demandé comment toute cette magie fonctionnait réellement ? Comment le système sait-il quand il faut mettre à jour l'écran et quelles parties doivent être mises à jour ?


Partie 1 : Contexte

Cette première partie de l’article présente quelques concepts clés qui seront ensuite utilisés pour mieux comprendre la deuxième partie de ce document.


Retour à l’appareil

Pour une fois, commençons par la fin et revenons à l’essentiel.

Lorsque vous regardez votre appareil, ou plus précisément votre application s’exécutant sur votre appareil, vous ne voyez qu’un écran.

En fait, tout ce que vous voyez est une série de pixels qui, ensemble, composent une image plate (2 dimensions) et lorsque vous touchez l'écran avec votre doigt, l’appareil ne reconnaît que la position de votre doigt sur le verre.

Toute la magie de l’application (d’un point de vue visuel) consiste à faire actualiser cette image plate en fonction, la plupart du temps, des interactions avec :

  • l'écran (par exemple, le doigt sur le verre)
  • le réseau (par exemple, la communication avec un serveur)
  • l’heure (par exemple les animations)
  • autres capteurs externes

Le rendu de l’image à l'écran est assuré par le hardware (display device), qui à intervalle régulier (généralement 60 fois par seconde), rafraîchit l’affichage. Cette fréquence de rafraîchissement est également appelée “fréquence de rafraîchissement” et est exprimée en Hz (Hertz).

Le display device reçoit les informations à afficher sur l'écran via le GPU (Graphics Processing Unit), qui est un circuit électronique spécialisé, optimisé et conçu pour générer rapidement une image à partir de certaines données (polygones et textures). Le nombre de fois par seconde que le GPU est capable de générer l’image (=frame buffer) à afficher et à envoyer au hardware est appelé le frame rate. Celui-ci est mesuré avec l’unité fps (par exemple 60 images par seconde ou 60 fps).

Vous me demanderez peut-être pourquoi j’ai commencé cet article avec les notions d’image plate en 2 dimensions, rendue par le GPU/matériel et le capteur physique en verre… et quelle est la relation avec les habituels Widgets de Flutter ?

Simplement parce que l’un des principaux objectifs d’une application Flutter est de composer cette image plate en 2 dimensions et de permettre d’interagir avec elle, je pense qu’il serait plus facile de comprendre comment Flutter fonctionne réellement si nous l’examinons sous cet angle.

…mais aussi parce que dans Flutter, croyez-le ou non, presque tout est piloté par la nécessité de devoir rafraîchir l'écran… rapidement et au bon moment !


Interface entre le code et le dispositif physique

Un jour ou l’autre, tous ceux qui s’intéressent à Flutter ont déjà vu la photo suivante qui décrit l’architecture de haut niveau de Flutter.

Architecture Flutter (c) Flutter

Lorsque nous écrivons une application Flutter, en utilisant Dart, nous restons au niveau du Flutter Framework (en vert).

Le Flutter Framework interagit avec le Flutter Engine (en bleu), via une couche d’abstraction, appelée Window. Cette couche d’abstraction expose une série d’API pour communiquer, indirectement, avec l’appareil.

C’est également via cette couche d’abstraction que le Flutter Engine notifie le Flutter Framework lorsque, entre autres :

  • un événement d’intérêt se produit au niveau de l’appareil (changement d’orientation, modification des paramètres, problème de mémoire, état d’exécution de l’application…)
  • un événement se produit au niveau du verre (= geste)
  • le canal de la plate-forme envoie des données
  • mais aussi et surtout, lorsque le Flutter Engine est prêt à rendre une nouvelle image

Le Flutter Framework est piloté par le Flutter Engine frame rendering

Cette affirmation est assez difficile à croire, mais c’est la vérité.

Sauf dans certains cas (voir ci-dessous), aucun code du Flutter Framework n’est exécuté sans avoir été déclenché par le Flutter Engine frame rendering.

Ces exceptions sont les suivantes :

  • Gesture (= un événement sur la vitre)
  • Messages de la plate-forme (= messages émis par l’appareil, par exemple le GPS)
  • Messages de l’appareil (= messages qui font référence à une variation de l'état de l’appareil, par exemple l’orientation, l’application envoyée en arrière-plan, les avertissements de la mémoire, les paramètres de l’appareil…)
  • Future ou réponses http.

Le Flutter Framework n’appliquera aucune modification visuelle sans avoir été demandée par le Flutter Engine frame rendering.

(entre nous, il est cependant possible d’appliquer un changement visuel sans avoir été invité par le moteur Flutter, mais ce n’est vraiment pas conseillé de le faire)

Mais vous allez me demander, si un code lié au gesture est exécuté et provoque un changement visuel, ou si j’utilise un timer pour rythmer une tâche, ce qui entraîne des changements visuels (comme une animation, par exemple), comment cela fonctionne-t-il alors ?

Si vous voulez qu’un changement visuel se produise, ou si vous voulez qu’un code soit exécuté sur la base d’un minuteur, vous devez indiquer au Flutter Engine que quelque chose doit être rendu.

Habituellement, lors du prochain rafraîchissement, le moteur Flutter demande au Flutter Framework d’exécuter du code et de fournir éventuellement la nouvelle scène à générer.

Par conséquent, la grande question est de savoir comment le moteur Flutter orchestre le comportement de l’application dans son ensemble, en fonction du rendu.

Pour vous donner une idée des mécanismes internes, regardez l’animation suivante…

Internals flow
Internals flow

Brève explication (des détails supplémentaires viendront plus tard) :

  • Certains événements externes (geste, réponses http, …) ou même futures, peuvent lancer des tâches qui conduisent à devoir mettre à jour le rendu. Un message est envoyé au Flutter Engine pour le notifier ( = Schedule Frame)
  • Lorsque le Flutter Engine est prêt à procéder à la mise à jour du rendu, il émet une requête Begin Frame.
  • Cette requête Begin Frame est interceptée par le Flutter Framework, qui exécute toute tâche principalement liée aux Tickers (comme les Animations, par exemple)
  • Ces tâches pourraient réémettre une demande de rendu d’une image ultérieure… (exemple : une animation n’est pas terminée et pour avancer, elle devra recevoir une autre Begin frame à un stade ultérieur)
  • Ensuite, le Flutter Engine émet un Draw Frame.
  • Ce Draw Frame est intercepté par le Flutter Framework, qui va rechercher toutes les tâches liées à la mise à jour de la mise en page en termes de structure et de taille.
  • Une fois toutes ces tâches terminées, il continue avec les tâches liées à la mise à jour de la mise en page en termes de “painting".
  • S’il y a quelque chose à dessiner sur l'écran, il envoie alors la nouvelle scène à rendre au Flutter Engine qui mettra à jour l'écran.
  • Ensuite, le Flutter Framework exécute toutes les tâches à exécuter une fois le rendu terminé (= PostFrame callbacks) et toutes les autres tâches secondaires non liées au rendu.
  • … et ce flux recommence encore et encore.

RenderView et RenderObject

Avant d’entrer dans les détails relatifs au déroulement des actions, il est opportun d’introduire la notion de Rendering Tree.

Comme nous l’avons dit précédemment, tout finit par devenir une série de pixels à afficher à l'écran et le Flutter Framework convertit les Widgets que nous utilisons pour développer l’application en parties visuelles qui seront rendues à l'écran.

Ces parties visuelles qui sont rendues à l'écran correspondent à des objets, appelés RenderObjects, qui sont utilisés afin de:

  • définir une partie de l'écran en termes de dimensions, de position, de géométrie mais aussi en termes de “contenu rendu".
  • identifier les zones de l'écran potentiellement touchées par les gestes (= doigt)

L’ensemble de tous les RenderObject forme un arbre, appelé Render Tree. Au sommet de cet arbre (= root), on trouve un RenderView.

Le RenderView représente la surface visuelle totale du Render Tree et est elle-même une version spéciale d’un RenderObject.

Visuellement parlant, nous pourrions représenter tout cela comme suit :

RenderView - RenderObject

La relation entre Widgets et RenderObjects sera abordée plus loin dans cet article.

Il est maintenant temps d’aller un peu plus loin…


Tout d’abord, l’initialisation des ‘bindings’

Lorsque vous lancez une application Flutter, le système invoque la méthode main() qui finira par appeler la méthode runApp(Widget app).

Pendant cet appel à la méthode runApp(), Flutter Framework initialise les interfaces entre le Flutter Framework et le Flutter Engine. Ces interfaces sont appelées bindings.

Les Bindings - Introduction

Les bindings sont censés être une sorte de colle entre le Flutter Engine et le Flutter Framework. Ce n’est qu'à travers ces bindings que les données peuvent être échangées entre les deux parties de Flutter (Engine et Framework).
(Il n’y a qu’une seule exception à cette règle : la RenderView mais nous verrons cela plus tard).

Chaque Binding est responsable de la gestion d’un ensemble de tâches, d’actions, d'événements spécifiques, regroupés par domaine d’activités.

Au moment de la rédaction de cet article, Flutter Framework compte 8 Bindings.

Ci-dessous, les 4 qui seront abordés dans cet article :

  • SchedulerBinding
  • GestureBinding
  • RendererBinding
  • WidgetsBinding

Par souci d’exhaustivité, les 4 derniers (qui ne seront pas abordés dans cet article) :

  • ServicesBinding : responsable du traitement des messages envoyés par le canal *plateforme
  • PaintingBinding : responsable de la gestion du cache des images
  • SemanticsBinding : réservé pour une mise en œuvre ultérieure de tout ce qui concerne Semantics.
  • TestWidgetsFlutterBinding : utilisé par la bibliothèque de tests de widgets

Je pourrais aussi mentionner le WidgetsFlutterBinding mais ce dernier n’est pas vraiment un binding mais plutôt une sorte de “initialisateur de binding".

Le schéma suivant montre les interactions entre les bindings que je vais aborder un peu plus loin dans cet article et le Flutter Engine.

Interactions des bindings

Examinons chacune de ces bindings"principaux”.


SchedulerBinding

Ce binding a 2 responsabilités principales :

  • la première est de le dire au Flutter Engine : “Hey ! la prochaine fois que vous n'êtes pas occupé, réveillez-moi pour que je puisse travailler un peu et vous dire soit ce qu’il faut rendre, soit si j’ai besoin que vous me rappeliez plus tard…” ;

  • la seconde est d'écouter et de réagir à de tels “rappels” (voir plus loin)

Quand le SchedulerBinding demande-t-il un rappel ?

  • Quand un Ticker doit ‘ticker
    Par exemple, supposons que vous ayez une animation et que vous la lanciez. Une animation est cadencée par un Ticker, qui à intervalle régulier (= tick) est appelé à effectuer un callback. Pour exécuter un tel callback, nous devons dire au Flutter Engine de nous réveiller au prochain rafraîchissement (= Begin Frame). Ceci invoquera le callback pour effectuer sa tâche. A la fin de cette tâche, si le ticker a encore besoin d’avancer, il appellera le SchedulerBinding pour programmer une autre frame.

  • Lorsqu’un changement s’applique à la mise en page
    Lorsque, par exemple, vous réagissez à un événement qui entraîne un changement visuel (par exemple, mise à jour de la couleur d’une partie de l'écran, défilement, ajout/suppression d’un élément à l'écran), nous devons prendre les mesures nécessaires pour le rendre éventuellement à l'écran. Dans ce cas, lorsqu’un tel changement se produit, le Flutter Framework invoquera le SchedulerBinding pour programmer une autre image avec le Flutter Engine (nous verrons plus tard comment cela fonctionne réellement).


Obligation de faire des gestes

Ce binding écoute les interactions avec le moteur en termes de “doigt". (= gesture).

Il est notamment chargé d’accepter les données relatives au doigt et de déterminer quelle(s) partie(s) de l'écran est (sont) touchée(s) par les gestes. Il notifie ensuite cette/ces partie(s) en conséquence.


RendererBinding

Ce binding est la colle entre le Flutter Engine et le Render Tree. Il a 2 responsabilités distinctes :

  • la première est d'écouter les événements, émis par le moteur, pour informer sur les changements appliqués par l’utilisateur via les paramètres du dispositif, qui ont un impact sur les visuels et/ou les sémantiques.

  • la seconde consiste à fournir au moteur les modifications à appliquer à l’affichage.

Afin de fournir les modifications à rendre à l'écran, ce Binding est chargé de piloter le PipelineOwner et d’initialiser le RenderView.

Le PipelineOwner est une sorte de orchestrateur qui sait quel RenderObject a besoin de faire quelque chose en relation avec la mise en page et coordonne ces actions.


WidgetsBinding

Ce binding écoute les changements appliqués par l’utilisateur via les paramètres de l’appareil, qui ont un impact sur la langue (= locale) et la sémantique.

Note

A un stade ultérieur, je suppose que tous les événements liés à la Sémantique seront migrés vers la SémantiqueBinding mais au moment de la rédaction de cet article, ce n’est pas encore le cas.

En outre, le WidgetsBinding est la colle entre les Widgets et le Flutter Engine. Il a 2 responsabilités principales distinctes :

  • la première principale est de piloter le processus chargé de gérer les changements de structure des Widgets

  • la seconde consiste à déclencher le rendu

La gestion des modifications de la structure des widgets se fait via le BuildOwner.

Le BuildOwner identifie les Widgets à reconstruire et gère les autres tâches qui s’appliquent à l’ensemble de la structure des Widgets.


Partie 2 : des Widgets aux pixels

Maintenant que nous avons introduit les bases de la mécanique interne, il est temps de parler des Widgets.

Dans toute la documentation de Flutter, vous lirez que tout est Widgets.

C’est presque exact, mais pour être un peu plus précis, je dirais plutôt

Du point de vue du développeur, tout ce qui concerne l’interface utilisateur en termes de mise en page et d’interaction, se fait via des Widgets.

Pourquoi cette précision ? Parce qu’un Widget permet à un développeur de définir une partie de l'écran en termes de dimensions, de contenu, de mise en page et d’interaction mais il y a tellement plus. Alors, qu’est-ce qu’un Widget, en fait ?


Configuration immuable

Lorsque vous lisez le code source de Flutter, vous remarquerez la définition suivante de la classe Widget.

@immutable
abstract class Widget extends DiagnosticableTree {
  const Widget({ this.key });

  final Key key;

  ...
}

Qu’est-ce que cela signifie ?

L’annotation “@immuable” est très importante et nous dit que toute variable d’une classe de Widget doit être FINALE, en d’autres termes : “est définie et assignée UNE FOIS POUR TOUTE". Ainsi, une fois instancié, le Widget ne pourra plus modifier ses variables internes.

Un Widget est une sorte de configuration invariable puisqu’il est IMMUTABLE


La structure hiérarchique des Widgets

Lorsque vous développez avec Flutter, vous définissez la structure de votre (vos) écran(s), en utilisant des Widgets… Quelque chose comme :

Widget build(BuildContext context){
    return SafeArea(
        child: Scaffold(
            appBar: AppBar(
                title: Text('My title'),
            ),
            body: Container(
                child: Center(
                    child: Text('Centered Text'),
                ),
            ),
        ),
    );
}

Cet exemple utilise 7 Widgets, qui forment ensemble une structure hiérarchique. La structure très simplifiée, basée sur le code, est la suivante :

Arbre des Widgets simplifié

Comme vous pouvez le voir, cela ressemble à un arbre, où le SafeArea est la racine de l’arbre.


La forêt derrière l’arbre

Comme vous le savez déjà, un Widget peut lui-même être une agrégation d’autres Widgets. A titre d’exemple, j’aurais pu écrire le code précédent de la façon suivante :

Widget build(BuildContext context){
    return MyOwnWidget();
}

Cela présuppose que le widget “MyOwnWidget” rendrait lui-même la SafeArea, Scaffold… mais le plus important avec cet exemple est que

un Widget peut-être une feuille, un nœud dans un arbre, voire un arbre lui-même ou pourquoi pas une forêt d’arbres…


La notion d'élément dans l’arbre

Pourquoi ai-je mentionné cela ?

Comme nous le verrons plus tard, afin de pouvoir générer les pixels qui composent l’image à rendre à l'écran, Flutter a besoin de connaître en détail toutes les petites parties qui composent l'écran et, pour déterminer toutes les parties, il demandera de gonfler(= inflate) tous les Widgets.

Pour illustrer ceci, considérons le principe des poupées russes : fermée vous ne voyez qu’une poupée mais celle-ci en contient une autre qui en contient une autre à son tour et ainsi de suite…

poupées russes

Lorsque Flutter aura gonflé tous les widgets, une partie de l'écran, ce sera comme obtenir toutes les différentes poupées russes, une partie de l’ensemble.

Le schéma suivant montre une partie de la structure hiérarchique finale des Widgets qui correspond au code précédent. En jaune, j’ai surligné les Widgets qui ont été mentionnés dans le code afin que vous puissiez les repérer dans l’arbre partiel des Widgets qui en résulte.

Inflated Widgets

Clarification importante

L’expression “Arbre de Widgets” n’existe que pour faciliter la compréhension puisque les programmeurs utilisent des Widgets mais, dans Flutter, il n’y a PAS d’arbre de Widgets !

En fait, pour être exact, nous devrions plutôt dire : “arbre des Elements

Il est maintenant temps d’introduire la notion d’ Element

A chaque widget correspond un élément. Les éléments sont liés les uns aux autres et forment un arbre. Par conséquent, un élément est une référence de quelque chose dans l’arbre.

Au début, pensez à un élément comme un noeud qui a un parent et potentiellement un enfant. Reliés entre eux par la relation parent, nous obtenons une structure arborescente.

Un élément

Comme vous pouvez le voir dans l’image ci-dessus, l’ Element ** pointe vers** un *Widget* et **peut** aussi pointer vers un *RenderObject*.

Encore mieux… l'élément pointe vers le Widget qui a créé l'élément !

Laissez-moi récapituler…

  • Il n’y a pas d’arbre de Widgets mais un arbre d’Eléments
  • Les éléments sont créés par les Widgets
  • un élément fait référence au Widget qui l’a créé
  • Les éléments sont liés entre eux par la relation parent.
  • Les éléments pourraient avoir un ou plusieurs enfants
  • Des éléments pourraient également indiquer un RenderObject.

Les éléments définissent comment les parties visuelles sont liées entre elles

Afin de mieux visualiser où se situe la notion d’ élément, considérons la représentation visuelle suivante :

Les 3 arbres
Les 3 arbres

Comme vous pouvez le voir, l’arbre des éléments est le lien réel entre les Widgets et les RenderObjects.

Mais, pourquoi le Widget crée-t-il l'Élément ?


3 grandes catégories de Widgets

Dans Flutter, les Widgets sont divisés en 3 catégories principales, que j’appelle personnellement :
(mais ce n’est que ma façon de les catégoriser)

  • les ‘proxies’

    Le rôle principal de ces Widgets est de contenir une information qui doit être mise à la disposition des Widgets, qui font partie de l’arborescence, et qui sont rattachés aux proxies. Un exemple typique de ces Widgets est le InheritedWidget ou LayoutId.

    Ces Widgets ne font pas directement partie de l’interface utilisateur, mais sont utilisés par d’autres pour récupérer les informations qu’ils peuvent fournir.

  • les ‘renderers’

    Ces Widgets ont une implication directe dans le layout de l'écran car ils définissent (ou sont utilisés pour déduire) soit :

    • les dimensions ;
    • la position ;
    • l’aspect, le rendu.

    Voici des exemples typiques : Row, Column, Stack mais aussi Padding, Align, Opacity, RawImage

  • les composants.

    Ce sont les autres Widgets qui ne fournissent pas directement les informations finales relatives aux dimensions, aux positions, à l’aspect mais plutôt des données (ou indice) qui seront utilisées pour obtenir les informations finales. Ces Widgets sont communément appelés composantes.

    En voici quelques exemples : RaisedButton, Scaffold, Text, GestureDetector, Container

Widgets Categories
Widgets Categories

La liste suivante PDF énumère la plupart des Widgets, regroupés par catégories.

Pourquoi cette répartition est-elle importante ? Parce qu’en fonction de la catégorie de Widget, un type d’Element correspondant est associé…


Les types d'éléments

Voici les différents types d'éléments :

Internals Element Types
Internals Element Types

Comme vous pouvez le voir sur l’image ci-dessus, les éléments sont répartis en 2 types principaux :

  • ComponentElement

    Ces éléments ne correspondent directement à aucune partie du rendu visuel.

  • RenderObjectElement

    Ces éléments correspondent à une partie de l'écran rendu.

Génial ! Beaucoup d’informations jusqu'à présent, mais comment tout cela est-il lié et pourquoi était-il intéressant de présenter tout cela ?


Comment les widgets et les éléments fonctionnent-ils ensemble ?

Dans Flutter, toute la mécanique repose sur l’invalidation d’un élément ou d’un renderObject.

L’invalidation d’un élément peut se faire de différentes manières :

  • en utilisant setState, qui invalide l’ensemble du StatefulElement (notez que je ne dis pas intentionnellement StatefulWidget)
  • via des notifications, traitées par d’autres proxyElements (comme InheritedWidget, par exemple), qui invalident tout élément qui dépend de ce proxyElement.

Le résultat d’une invalidation est que le(s) élément(s) correspondant(s) est/sont référencé(s) dans une liste de dirty éléments.

L’invalidation d’un renderObject signifie qu’aucune modification n’est appliquée à la structure des éléments mais qu’une modification au niveau d’un renderObject se produit, telle que

  • modifie ses dimensions, sa position, sa géométrie…
  • doit être repeint, par exemple lorsque vous changez simplement la couleur de fond, le style de police…

Le résultat d’une telle invalidation est que le renderObject correspondant est référencé dans une liste de renderObjects qui doivent être reconstruits ou repeints.

Quel que soit le type d’invalidation, lorsque cela se produit, le SchedulerBinding (vous vous en souvenez ?) est prié de demander au Flutter Engine de programmer une nouvelle frame.

C’est lorsque le Flutter Engine réveille le SchedulerBinding que toute la magie se produit…

onDrawFrame()

Plus tôt dans cet article, nous avons mentionné que le SchedulerBinding avait 2 responsabilités principales, dont l’une était d'être prêt à traiter les demandes émises par le Flutter Engine, liées à la reconstruction des frames. C’est le moment idéal pour se concentrer sur ce point maintenant…

Le diagramme de séquence partielle ci-dessous montre ce qui se passe lorsque le SchedulerBinding reçoit une requête onDrawFrame() du Flutter Engine.

Internals onDrawFrame() - Elements
Internals onDrawFrame() - Elements
Etape 1 : les éléments

Le WidgetsBinding est invoqué et ce dernier prend d’abord en compte les changements liés aux éléments.

Comme le BuildOwner est responsable de la gestion de l’arbre des éléments, le WidgetsBinding invoque la méthode buildScope du buildOwner.

Cette méthode passe en renvue la liste des éléments invalidés (= sales) et leur demande de se reconstruire.

Les grands principes de cette méthode rebuild() sont les suivants :

  1. demander à l'élément de rebuild() ce qui conduit la plupart du temps, à invoquer la méthode build() du widget référencé par cet élément (= méthode Widget build(BuildContext context){…}). Cette méthode build() renvoie un nouveau widget.
  2. si l'élément n’a pas de fils, le nouveau widget est gonflé (voir juste après), sinon
  3. le nouveau widget est comparé à celui référencé par l’enfant de l'élément.
    • S’ils ont pu être échangés (=même type de widget et même clé), la mise à jour est effectuée, l'élément enfant est conservé.
    • S’ils n’ont pas pu être échangés, l'élément enfant est démonté (~ discarded) et le nouveau widget est gonflé.
  4. Le gonflage du widget entraîne la création d’un nouvel élément, qui est monté en tant que nouvel enfant de l'élément. (montés = insérés dans l’arbre des éléments)

L’animation suivante tente de rendre cette explication un peu plus visuelle.

onDrawFrame() - Elements
onDrawFrame() - Elements

Note sur le gonflement des widgets

Lorsque le widget est gonflé, il lui est demandé de créer un nouvel élément d’un certain type, défini par la catégorie widget.

Par conséquent,

  • un InheritedWidget générera un InheritedElement.
  • un StatefulWidget générera un StatefulElement
  • un StatelessWidget générera un StatelessElement
  • Un InheritedModel générera un InheritedModelElement
  • un InheritedNotifier générera un InheritedNotifierElement
  • un LeafRenderObjectWidget générera un LeafRenderObjectElement
  • un SingleChildRenderObjectWidget générera un SingleChildRenderObjectElement
  • un MultiChildRenderObjectWidget générera un MultiChildRenderObjectElement
  • un ParentDataWidget générera un ParentDataElement

Chacun de ces types de éléments a un comportement distinct.

Par exemple

  • a StatefulElement invoquera la méthode widget.createState() à l’initialisation, ce qui créera le State et le liera au element
  • un type RenderObjectElement créera un RenderObject lorsque l'élément sera monté, ce renderObject sera ajouté à au render tree et lié à l’ élément.

Etape 2 : les renderObjects

Une fois que toutes les actions liées aux *éléments ont été réalisées, l’arbre des éléments est maintenant stable et il est temps de considérer le processus de rendu.

Comme le RendererBinding est responsable de la gestion du rendering tree, le WidgetsBinding invoque la méthode drawFrame du RendererBinding.

Le diagramme partiel ci-dessous montre la séquence des actions effectuées lors d’une requête drawFrame().

Internals onDrawFrame() - RenderObjects
Internals onDrawFrame() - RenderObjects

Au cours de cette étape, les activités suivantes sont effectuées :

  • chaque renderObject marqué comme dirty est invité à effectuer sa mise en page (c’est-à-dire à calculer ses dimensions et sa géométrie)
  • Chaque renderObject marqué comme a besoin d'être repeint est repeint, en utilisant la couche de renderObject.
  • la scène résultante est construite et envoyée au Flutter Engine afin que ce dernier la transmette à l'écran de l’appareil.
  • Enfin, la Sémantique est également mise à jour et envoyée au Flutter Engine.

A la fin de ce processus, l'écran de l’appareil est mis à jour.


Partie 3 : Traitement des gestes

Les gestes (= événements liés au doigt sur le verre) sont traités par le GestureBinding.

Lorsque le Flutter Engine envoie des informations relatives à un événement lié à un geste, via l’API window.onPointerDataPacket, le GestureBinding l’intercepte, procède à un certain buffering et :

  1. convertit les coordonnées émises par le Flutter Engine pour qu’elles correspondent au rapport de pixels du dispositif( = pixel ratio), puis
  2. demande au renderView de fournir une liste de TOUS les RenderObjects qui couvrent une partie de l'écran contenant les coordonnées de l'événement
  3. ensuite, il parcourt cette liste de renderObjects et envoie l'événement correspondant à chacun d’eux.
  4. lorsqu’un renderObject attend ce type d'événement, il le traite.

A partir de cette explication, nous voyons directement l’importance des renderObjects


Partie 4 : Animations

Cette dernière partie de l’article se concentre sur la notion de Animations et plus particulièrement sur la notion de Ticker.

Lorsque vous lancez une animation, vous utilisez généralement un AnimationController ou tout autre Widget ou composant similaire.

Dans Flutter, tout ce qui est lié à l’animation fait référence à la notion de Ticker.

Un Ticker ne fait qu’une seule chose, lorsqu’il est actif : “il demande au SchedulerBinding d’enregistrer un rappel et de demander au moteur Flutter de le réveiller lors de la prochaine disponibilité".

Lorsque le Flutter Engine est prêt, il invoque alors le SchedulerBinding via une requête : “onBeginFrame".

Le SchedulerBinding intercepte cette requête puis parcourt toute la liste des rappels de tickers et invoque chacun d’entre eux.

Chaque ticker ‘tick’ est intercepté par tout contrôleur intéressé par cet événement pour le traiter. Si l’animation est terminée, le ticker est “désactivé", sinon, le ticker demande au SchedulerBinding de programmer un autre rappel. Et ainsi de suite…


Image globale

Maintenant que nous avons vu comment fonctionnent les internes de Flutter, voici la situation globale :

Internals Big Picture
Internals Big Picture

BuildContext

Le mot de la fin…

Si vous vous souvenez du diagramme qui montre les différents types de éléments, vous avez très probablement remarqué la signature du élément de base :

abstract class Element extends DiagnosticableTree implements BuildContext {
    ...
}

Voici le fameux BuildContext.

Qu’est-ce qu’un BuildContext ?

Le BuildContext est une interface qui définit une série de getters et de methodes qui pourraient être mis en œuvre par un élément, de manière harmonisée.

En particulier, le BuildContext est principalement utilisé dans la méthode build() d’un StatelessWidget et StatefulWidget ou dans un objet StatefulWidget State.

Le BuildContext n’est rien d’autre que le Element lui-même qui correspond à

  • le Widget en cours de reconstruction (à l’intérieur des méthodes de construction ou du constructeur)
  • le StatefulWidget lié au State où vous faites référence à la variable contexte.

Cela signifie que la plupart des développeurs manipulent constamment des éléments, même sans le savoir.

Quelle peut être l’utilité du BuildContext ?

Comme le BuildContext correspond au élément lié au widget mais aussi à un emplacement du widget dans l’arbre, ce BuildContext est très utile pour :

  • obtenir la référence du RenderObject qui correspond au widget (ou si le widget n’est pas un renderer, le widget descendant)
  • obtenir la taille du RenderObject
  • visiter l’arbre. Ceci est en fait utilisé par tous les Widgets qui implémentent habituellement la méthode of (par exemple MediaQuery.of(context), Theme.of(context)…)

Juste pour le plaisir…

Maintenant que nous avons compris que le BuildContext est l’ élément, pour le plaisir, je voulais vous montrer une autre façon de l’utiliser…

Le code suivant totalement inutile permet à un StatelessWidget de se mettre à jour (comme si c'était un StatefulWidget mais sans utiliser aucun setState()), en utilisant le BuildContext

ATTENTION

Ne pas utiliser ce code !

Son seul but est de démontrer qu’un StatelessWidget est capable de demander à être reconstruit.

Si vous devez envisager un état avec un Widget, veuillez utiliser un StatefulWidget

void main(){
    runApp(MaterialApp(home: TestPage(),));
}

class TestPage extends StatelessWidget {
    // final because a Widget is immutable (remember?)
    final bag = {"first": true};

    @override
    Widget build(BuildContext context){
        return Scaffold(
            appBar: AppBar(title: Text('Stateless ??')),
            body: Container(
                child: Center(
                    child: GestureDetector(
                        child: Container(
                            width: 50.0,
                            height: 50.0,
                            color: bag["first"] ? Colors.red : Colors.blue,
                        ),
                        onTap: (){
                            bag["first"] = !bag["first"];
                            //
                            // This is the trick
                            //
                            (context as Element).markNeedsBuild();
                        }
                    ),
                ),
            ),
        );
    }
}

Entre nous, lorsque vous invoquez la méthode setState(), cette dernière finit par faire exactement la même chose : _element.markNeedsBuild().


Conclusions

Encore un long article, me direz-vous.

J’ai pensé qu’il pourrait être intéressant de savoir comment Flutter a été architecturé et de rappeler que tout a été conçu pour être efficace, évolutif et ouvert aux futures extensions.

De plus, des concepts clés tels que Widget, Element, BuildContext, RenderObject ne sont pas toujours évidents à appréhender.

J’espère que cet article aura pu être utile.

Restez à l'écoute pour de nouveaux articles, bientôt. En attendant, laissez-moi vous souhaiter un bon codage.