Flotter sous le capot

Bonjour à tous! Je m'appelle Mikhail Zotiev, je travaille en tant que développeur Flutter chez Surf. Moi, comme probablement la majorité des autres développeurs qui travaillent avec Flutter, j'aime surtout combien il est facile de créer des applications belles et pratiques avec son aide. Il faut très peu de temps pour entrer dans le développement de Flutter. J'ai récemment travaillé dans le développement de jeux, et maintenant je suis complètement passé au développement mobile multiplateforme sur Flutter.

Quelle est la simplicité? Avec une douzaine de widgets de base, vous pouvez créer des interfaces utilisateur assez décentes. Et au fil du temps, lorsque les bagages utilisés sont assez décents, il est peu probable qu'une tâche vous arrête: que ce soit un design inhabituel ou une animation sophistiquée. Et le plus intéressant - très probablement vous pouvez l'utiliser sans même penser à la question: "Comment ça marche du tout?"

Depuis Flutter a l'open source, j'ai décidé de comprendre ce qui est sous le capot (du côté Dart de la Force) et de le partager avec vous.



Widget


Nous avons tous entendu plus d'une fois la phrase de l'équipe de développement du framework: "Tout dans Flutter est un widget . " Voyons voir si c'est vraiment le cas. Pour ce faire, nous nous tournons vers la classe Widget (ci-après - le widget) et commençons progressivement à nous familiariser avec le contenu.

La première chose que nous lirons dans la documentation de la classe:
Décrit la configuration d'un [élément].

Il s'avère que le widget lui-même n'est qu'une description d'un élément (ci-après - l'élément).
Les widgets sont la hiérarchie centrale des classes dans le framework Flutter. Un widget est une description immuable d'une partie d'une interface utilisateur. Les widgets peuvent être gonflés en éléments, qui gèrent l'arborescence de rendu sous-jacente.
Pour résumer, l'expression "Tout dans Flutter est un widget" est le niveau minimum de compréhension de la façon dont tout est organisé pour utiliser Flutter. Le widget est la classe centrale de la hiérarchie Flutter. En même temps, il existe de nombreux mécanismes supplémentaires qui aident le cadre à faire face à sa tâche.

Nous avons donc appris quelques faits supplémentaires:

  • widget - une description immuable d'une partie de l'interface utilisateur;
  • le widget est associé à une vue avancée appelée élément;
  • un élément contrôle une entité de l'arbre de rendu.

Vous devez avoir remarqué une chose étrange. L'interface utilisateur et l'immuabilité sont très mal liées, je dirais même que ce sont des concepts totalement incompatibles. Mais nous y reviendrons quand une image plus complète de l'appareil du monde Flutter émergera, mais pour l'instant, nous continuerons à nous familiariser avec la documentation du widget.
Les widgets eux-mêmes n'ont pas d'état mutable (tous leurs champs doivent être définitifs).
Si vous souhaitez associer un état mutable à un widget, envisagez d'utiliser un [StatefulWidget], qui crée un objet [State] (via [StatefulWidget.createState]) chaque fois qu'il est gonflé dans un élément et incorporé dans l'arborescence.
Ce paragraphe complète un peu le premier paragraphe: si nous avons besoin d'une configuration mutable, nous utilisons l'entité spéciale State (ci-après dénommée l'état), qui décrit l'état actuel de ce widget. Cependant, l'état n'est pas associé au widget, mais à sa représentation élémentaire.
Un widget donné peut être inclus dans l'arbre zéro ou plusieurs fois. En particulier, un widget donné peut être placé plusieurs fois dans l'arborescence. Chaque fois qu'un widget est placé dans l'arbre, il est gonflé dans un [Element], ce qui signifie qu'un widget qui est incorporé plusieurs fois dans l'arbre sera gonflé plusieurs fois.
Le même widget peut être inclus plusieurs fois dans l'arborescence des widgets, ou ne pas être inclus du tout. Mais chaque fois qu'un widget est inclus dans l'arborescence des widgets, un élément lui est mappé.

Donc, à ce stade, les widgets sont presque terminés, résumons:

  • widget - la classe centrale de la hiérarchie;
  • widget est une configuration;
  • widget - une description immuable d'une partie de l'interface utilisateur;
  • le widget est associé à un élément qui contrôle le rendu d'une manière ou d'une autre;
  • l'état changeant du widget peut être décrit par une entité, mais il n'est pas connecté au widget, mais à l'élément qui représente ce widget.

Élément


D'après ce que nous avons appris, la question se pose: «Quels sont ces éléments qui régissent tout?» Faites de même - ouvrez la documentation de la classe Element.
Une instanciation d'un [Widget] à un emplacement particulier dans l'arborescence.
Un élément est une représentation d'un widget à un endroit spécifique dans un arbre.
Les widgets décrivent comment configurer un sous-arbre, mais le même widget peut être utilisé pour configurer plusieurs sous-arbres simultanément car les widgets sont immuables. Un [Element] représente l'utilisation d'un widget pour configurer un emplacement spécifique dans l'arborescence. Au fil du temps, le widget associé à un élément donné peut changer, par exemple, si le widget parent se reconstruit et crée un nouveau widget pour cet emplacement.
Le widget décrit la configuration d'une partie de l'interface utilisateur, mais comme nous le savons déjà, le même widget peut être utilisé à différents endroits de l'arborescence. Chacun de ces lieux sera représenté par un élément correspondant. Mais au fil du temps, le widget associé à l'élément peut changer. Cela signifie que les éléments sont plus tenaces et continuent d'être utilisés, ne mettant à jour que leurs connexions.

Il s'agit d'une décision assez rationnelle. Comme nous l'avons déjà défini ci-dessus, les widgets sont une configuration immuable qui décrit simplement une partie spécifique de l'interface, ce qui signifie qu'ils doivent être très légers. Et les éléments dans le domaine desquels la gestion est beaucoup plus lourde, mais ils ne sont pas recréés inutilement.

Pour comprendre comment cela se fait, considérez le cycle de vie d'un élément:

  • Widget.createElement , .
  • mount . .
  • .
  • , (, ), . runtimeType key, . , , .
  • , , , , ( deactivate).
  • , . , , (unmount), .
  • Lorsque vous ré-incluez des éléments dans l'arborescence, par exemple, si l'élément ou ses ancêtres ont une clé globale, elle sera supprimée de la liste des éléments inactifs, la méthode activate sera appelée et l'objet rendu associé à cet élément sera à nouveau incorporé dans l'arborescence de rendu. Cela signifie que l'élément devrait réapparaître à l'écran.

Dans la déclaration de classe, nous voyons que l'élément implémente l'interface BuildContext. Un BuildContext est quelque chose qui contrôle la position d'un widget dans une arborescence de widgets, comme suit dans sa documentation. Correspond presque exactement à la description de l'article. Cette interface est utilisée pour éviter la manipulation directe de l'élément, mais donne en même temps accès aux méthodes de contexte nécessaires. Par exemple, findRenderObject, qui vous permet de trouver l'objet d'arbre de rendu correspondant à cet élément.

Renderderbject


Reste à traiter du dernier maillon de cette triade - RenderObject . Comme son nom l'indique, il s'agit d'un objet de l'arbre de visualisation. Il a un objet parent, ainsi qu'un champ de données que l'objet parent utilise pour stocker des informations spécifiques concernant cet objet lui-même, par exemple, sa position. Cet objet est responsable de l'implémentation des protocoles de rendu et de mise en page de base.

RenderObject ne limite pas le modèle d'utilisation des objets enfants: il peut n'y en avoir aucun, un ou plusieurs. En outre, le système de positionnement n'est pas limité à: le système cartésien, les coordonnées polaires, tout cela et bien plus encore est disponible pour utilisation. Il n'y a pas de restrictions sur l'utilisation des protocoles de localisation: ajuster la largeur ou la hauteur, limiter la taille, spécifier la taille et l'emplacement du parent ou, si nécessaire, utiliser les données de l'objet parent.

Image du monde Flutter


Essayons de construire une vue d'ensemble de la façon dont tout fonctionne ensemble.

Nous l'avons déjà noté ci-dessus, le widget est une description immuable, mais l'interface utilisateur n'est pas du tout statique. Cet écart est supprimé en divisant en 3 niveaux d'objets et la division des zones de responsabilité.

  • , .
  • , .
  • , — , .

image

Voyons à quoi ces arbres ressemblent avec un exemple simple:

image

dans ce cas, nous avons un StatelessWidget enveloppé dans un widget Padding et contenant du texte à l'intérieur.

Mettons-nous à la place de Flutter - on nous a donné cet arbre de widget.

Flutter: "Hey, Padding, j'ai besoin de votre élément"
Padding: "Bien sûr, maintenez SingleChildRenderObjectElement"

image

Flutter: "Element, voici votre place, installez-vous"
SingleChildRenderObjectElement: "Les gars, tout va bien, mais j'ai besoin de RenderObject"
Flutter: "Padding, comme pour vous dessiner? "
Rembourrage: "Tenez-le, RenderPadding"
SingleChildRenderObjectElement: "Super, mettez -vous au travail"

image

Flutter:"Alors qui est le prochain?" StatelessWidget, maintenant vous laissez l'élément »
StatelessWidget: « Ici StatelessElement »
Flutter: « StatelessElement, vous serez soumis à SingleChildRenderObjectElement, voici l'endroit, embarquant »
StatelessElement: « OK »

image

Flutter: « le RichText, elementik Present, veuillez »
le RichText donne MultiChildRenderObjectElement
Flutter: «MultiChildRenderObjectElement, c'est parti, commencez»
MultiChildRenderObjectElement: «J'ai besoin d'un rendu pour le travail»
Flutter: «RichText, nous avons besoin d'un objet de rendu»
RichText: «Voici un RenderParagraph»
Flutter:"RenderParagraph vous recevrez des instructions RenderPadding, et MultiChildRenderObjectElement vous contrôlera"
MultiChildRenderObjectElement: "Maintenant tout va bien, je suis prêt"

image

Vous allez sûrement poser une question logique: "Où est l'objet de rendu pour StatelessWidget, pourquoi n'est-il pas là, nous avons décidé ci-dessus que les éléments lient les configurations avec écran? " Prenons attention à l'implémentation de base de la méthode de montage, qui a été discutée dans cette section de la description du cycle de vie.

void mount(Element parent, dynamic newSlot) {
    assert(_debugLifecycleState == _ElementLifecycle.initial);
    assert(widget != null);
    assert(_parent == null);
    assert(parent == null || parent._debugLifecycleState == _ElementLifecycle.active);
    assert(slot == null);
    assert(depth == null);
    assert(!_active);
    _parent = parent;
    _slot = newSlot;
    _depth = _parent != null ? _parent.depth + 1 : 1;
    _active = true;
    if (parent != null)
        _owner = parent.owner;
    if (widget.key is GlobalKey) {
        final GlobalKey key = widget.key;
        key._register(this);
    }
    _updateInheritance();
    assert(() {
        _debugLifecycleState = _ElementLifecycle.active;
        return true;
    }());
}

On n'y verra pas la création d'un objet de rendu. Mais l'élément implémente un BuildContext, qui a une méthode de recherche d'objet de visualisation findRenderObject, qui nous mènera au getter suivant:

RenderObject get renderObject {
    RenderObject result;
    void visit(Element element) {
        assert(result == null); 
        if (element is RenderObjectElement)
            result = element.renderObject;
        else
            element.visitChildren(visit);
    }
    visit(this);
    return result;
}

Dans le cas de base, un élément ne peut pas créer un objet de rendu; seuls RenderObjectElement et ses descendants sont requis pour ce faire, cependant, dans ce cas, un élément à un certain niveau d'imbrication doit avoir un élément enfant qui a un objet de rendu.

Il semblerait pourquoi toutes ces difficultés. Jusqu'à 3 arbres, différents domaines de responsabilité, etc. La réponse est assez simple - c'est là que les performances de Flutter sont construites. Les widgets sont des configurations immuables, par conséquent, ils sont souvent recréés, mais en même temps, ils sont assez légers, ce qui n'affecte pas les performances. Mais Flutter essaie de réutiliser autant que possible les éléments lourds.

Prenons un exemple.

Texte au milieu de l'écran. Dans ce cas, le code ressemblera à ceci:

body: Center(
    child: Text(“Hello world!”)
),

Dans ce cas, l'arborescence des widgets ressemblera à ceci: Une

image

fois que Flutter a construit les 3 arbres, nous obtenons l'image suivante:

image

Que se passe-t-il si nous modifions le texte que nous allons afficher?

image

Nous avons maintenant une nouvelle arborescence de widgets. Ci-dessus, nous avons parlé de la réutilisation maximale possible des éléments. Jetez un œil à la méthode de classe Widget, sous le nom parlant canUpdate .

static bool canUpdate(Widget oldWidget, Widget newWidget) {
    return oldWidget.runtimeType == newWidget.runtimeType && oldWidget.key == newWidget.key;
}

Nous vérifions le type du widget précédent et du nouveau, ainsi que leurs clés. S'ils sont identiques, il n'est pas nécessaire de modifier l'élément.

Donc, avant la mise à niveau, le premier élément est Centre, après la mise à niveau, également Centre. Les deux n'ont pas de clés, une coïncidence complète. Nous pouvons mettre à jour le lien de l'élément vers un nouveau widget.

image

Mais en plus du type et de la clé, le widget est une description et une configuration, et les valeurs des paramètres nécessaires à l'affichage peuvent changer. C'est pourquoi l'élément, après avoir mis à jour le lien vers le widget, doit lancer des mises à jour de l'objet de rendu. Dans le cas de Center, rien n'a changé et nous continuons de comparer davantage.

Encore une fois, le type et la clé nous indiquent qu'il est inutile de recréer l'élément. Le texte est un descendant de StatelessWidget; il n'a pas d'objet d'affichage direct.

image

Accédez à RichText. Le widget n'a pas non plus changé de type; il n'y a pas de divergence dans les clés. L'élément met à jour son association avec le nouveau widget.

image

La connexion est mise à jour, il ne reste plus qu'à mettre à jour les propriétés. Par conséquent, RenderParagraph affichera la nouvelle valeur de texte.

image

Et dès que le moment sera venu pour le prochain cadre de dessin, nous verrons le résultat que nous attendons.

Grâce à ce type de travail, Flutter atteint des performances aussi élevées.

L'exemple ci-dessus décrit le cas où la structure du widget elle-même n'a pas changé. Mais que se passe-t-il si la structure change? Flutter, bien sûr, continuera d'essayer de maximiser l'utilisation des objets existants, comme nous l'avons compris dans la description du cycle de vie, mais de nouveaux éléments seront créés pour tous les nouveaux widgets, et les anciens et les plus inutiles seront supprimés à la fin du cadre.

Regardons quelques exemples. Et pour nous assurer de ce qui précède, nous utilisons l'outil Android Studio - Flutter Inspector.

@override
Widget build(BuildContext context) {
    return Scaffold(
        body: Center(
            child: _isFirst ? first() : second(),
        ),
        floatingActionButton: FloatingActionButton(
            child: Text("Switch"),
            onPressed: () {
                setState(() {
                    _isFirst = !_isFirst;
                });
            },
        ),
    );
}

Widget first() => Row(
    mainAxisAlignment: MainAxisAlignment.center,
    children: <Widget>[
        Text(
            "test",
            style: TextStyle(fontSize: 25),
        ),
        SizedBox(
            width: 5,
        ),
        Icon(
            Icons.error,
        ),
    ],
);

Widget second() => Row(
    mainAxisAlignment: MainAxisAlignment.center,
    children: <Widget>[
        Text(
            "one more test",
            style: TextStyle(fontSize: 25),
        ),
        Padding(
            padding: EdgeInsets.only(left: 5),
        ),
        Icon(
            Icons.error,
        ),
    ],
);

Dans ce cas, en cliquant sur le bouton, l'un des widgets changera. Voyons ce que l'inspecteur nous montre.

image

image

Comme nous pouvons le voir, Flutter a recréé le rendu uniquement pour le remplissage, le reste vient d'être réutilisé.

Considérez 1 option de plus dans laquelle la structure change d'une manière plus globale - nous changeons les niveaux d'imbrication.

Widget second() => Container(child: first(),);

image

image

Malgré le fait que l'arbre n'a pas changé du tout visuellement, les éléments et les objets de l'arbre de rendu ont été recréés. Cela s'est produit parce que Flutter compare par niveau (dans ce cas, peu importe que la majeure partie de l'arbre n'ait pas changé), le filtrage de cette partie a eu lieu au moment de la comparaison du conteneur et de la ligne. Cependant, on peut sortir de cette situation. Cela nous aidera à GlobalKey. Ajoutez une telle clé pour Row.

var _key = GlobalKey(debugLabel: "testLabel");

Widget first() => Row(
    key: _key,
    …
);

image

image

Dès que nous avons dit à Flutter que la pièce pouvait être réutilisée, il en a profité.

Conclusion


Nous nous sommes un peu plus familiarisés avec la magie Flutter et nous savons maintenant que ce n'est pas seulement dans les widgets.

Flutter est un mécanisme bien coordonné bien pensé avec sa propre hiérarchie, ses domaines de responsabilité, avec lesquels vous pouvez créer non seulement de belles, mais aussi des applications productives. Bien sûr, nous n’avons examiné qu’une petite partie, quoique assez importante, de son dispositif, nous continuerons donc à analyser divers aspects du travail interne du cadre dans de futurs articles.

J'espère que les informations de cet article vous aideront à comprendre le fonctionnement interne de Flutter et vous aideront à trouver des solutions élégantes et productives pendant le développement.

Merci pour l'attention!

Ressources


Flutter
"Comment Flutter rend les widgets" par Andrew Fitz Gibbon, Matt Sullivan

All Articles