Aleteo bajo el capó

¡Hola a todos! Mi nombre es Mikhail Zotiev, trabajo como desarrollador de Flutter en Surf. Probablemente, como la mayoría de los otros desarrolladores que trabajan con Flutter, me gusta lo fácil que es crear aplicaciones hermosas y convenientes con su ayuda. Se necesita muy poco tiempo para entrar en el desarrollo de Flutter. Recientemente trabajé en desarrollo de juegos, y ahora he cambiado completamente al desarrollo móvil multiplataforma en Flutter.

¿Cuál es la simplicidad? Con una docena de widgets básicos, puede construir interfaces de usuario bastante decentes. Y con el tiempo, cuando el equipaje utilizado es bastante decente, es poco probable que alguna tarea lo detenga: ya sea un diseño inusual o una animación sofisticada. Y lo más interesante: lo más probable es que pueda usarlo sin siquiera pensar en la pregunta: "¿Cómo funciona?"

Como Flutter tiene código abierto, decidí averiguar qué hay debajo del capó (en el lado de Dart de la Fuerza) y compartirlo contigo.



Widget


Todos hemos escuchado la frase del equipo de desarrollo de framework más de una vez: "Todo en Flutter son widgets" . Veamos si esto es realmente así. Para hacer esto, pasamos a la clase Widget (en adelante, el widget) y comenzamos a familiarizarnos gradualmente con los contenidos.

Lo primero que leeremos en la documentación de la clase:
Describe la configuración para un [Elemento].

Resulta que el widget en sí mismo es solo una descripción de algún Elemento (en adelante, el elemento).
Los widgets son la jerarquía de clase central en el marco de Flutter. Un widget es una descripción inmutable de parte de una interfaz de usuario. Los widgets se pueden inflar en elementos, que administran el árbol de renderizado subyacente.
Para resumir, la frase "Todo en Flutter es un widget" es el nivel mínimo de comprensión de cómo se organiza todo para usar Flutter. El widget es la clase central en la jerarquía Flutter. Al mismo tiempo, hay muchos mecanismos adicionales a su alrededor que ayudan al marco a hacer frente a su tarea.

Entonces, aprendimos algunos hechos más:

  • widget: una descripción inmutable de una parte de la interfaz de usuario;
  • el widget está asociado con alguna vista avanzada llamada elemento;
  • un elemento controla alguna entidad del árbol de renderizado.

Debes haber notado algo extraño. La interfaz de usuario y la inmutabilidad encajan muy mal, incluso diría que estos son conceptos completamente incompatibles. Pero volveremos a esto cuando surja una imagen más completa del dispositivo del mundo Flutter, pero por ahora seguiremos familiarizándonos con la documentación del widget.
Los widgets no tienen un estado mutable (todos sus campos deben ser finales).
Si desea asociar el estado mutable con un widget, considere usar un [StatefulWidget], que crea un objeto [State] (a través de [StatefulWidget.createState]) cada vez que se infla en un elemento y se incorpora al árbol.
Este párrafo complementa un poco el primer párrafo: si necesitamos una configuración mutable, usamos la entidad de Estado especial (en lo sucesivo, el estado), que describe el estado actual de este widget. Sin embargo, el estado no está asociado con el widget, sino con su representación elemental.
Un widget determinado se puede incluir en el árbol cero o más veces. En particular, un widget determinado se puede colocar en el árbol varias veces. Cada vez que se coloca un widget en el árbol, se infla en un [Elemento], lo que significa que un widget que se incorpora al árbol varias veces se inflará varias veces.
El mismo widget se puede incluir en el árbol de widgets muchas veces, o no se puede incluir en absoluto. Pero cada vez que se incluye un widget en el árbol de widgets, se le asigna un elemento.

Entonces, en esta etapa, los widgets están casi listos, resumamos:

  • widget: la clase central de la jerarquía;
  • widget es alguna configuración;
  • widget: una descripción inmutable de una parte de la interfaz de usuario;
  • el widget está asociado con un elemento que controla la representación de alguna manera;
  • alguna entidad puede describir el estado cambiante del widget, pero no está conectado con el widget, sino con el elemento que representa este widget.

Elemento


De lo que aprendimos, la pregunta plantea: "¿Cuáles son estos elementos que rigen todo?" Haga lo mismo: abra la documentación para la clase Element.
Una instanciación de un [Widget] en una ubicación particular del árbol.
Un elemento es alguna representación de un widget en un lugar específico de un árbol.
Los widgets describen cómo configurar un subárbol, pero el mismo widget se puede usar para configurar múltiples subárboles simultáneamente porque los widgets son inmutables. Un [Elemento] representa el uso de un widget para configurar una ubicación específica en el árbol. Con el tiempo, el widget asociado con un elemento dado puede cambiar, por ejemplo, si el widget padre se reconstruye y crea un nuevo widget para esta ubicación.
El widget describe la configuración de alguna parte de la interfaz de usuario, pero como ya sabemos, el mismo widget se puede usar en diferentes lugares del árbol. Cada uno de esos lugares estará representado por un elemento correspondiente. Pero con el tiempo, el widget asociado con el elemento puede cambiar. Esto significa que los elementos son más tenaces y continúan utilizándose, solo actualizando sus conexiones.

Esta es una decisión bastante racional. Como ya hemos definido anteriormente, los widgets son una configuración inmutable que simplemente describe una parte específica de la interfaz, lo que significa que deben ser muy livianos. Y los elementos en el área cuyo control es mucho más pesado, pero no se recrean innecesariamente.

Para entender cómo se hace esto, considere el ciclo de vida de un elemento:

  • Widget.createElement , .
  • mount . .
  • .
  • , (, ), . runtimeType key, . , , .
  • , , , , ( deactivate).
  • , . , , (unmount), .
  • Cuando vuelva a incluir elementos en el árbol, por ejemplo, si el elemento o sus antepasados ​​tienen una clave global, se eliminará de la lista de elementos inactivos, se llamará al método de activación y el objeto renderizado asociado con este elemento volverá a incrustarse en el árbol de representación. Esto significa que el elemento debería aparecer nuevamente en la pantalla.

En la declaración de clase, vemos que el elemento implementa la interfaz BuildContext. Un BuildContext es algo que controla la posición de un widget en un árbol de widgets, como se deduce de su documentación. Casi coincide exactamente con la descripción del artículo. Esta interfaz se utiliza para evitar la manipulación directa del elemento, pero al mismo tiempo da acceso a los métodos de contexto necesarios. Por ejemplo, findRenderObject, que le permite encontrar el objeto del árbol de representación correspondiente a este elemento.

Objeto de renderizado


Queda por tratar con el último enlace de esta tríada: RenderObject . Como su nombre lo indica, este es un objeto del árbol de visualización. Tiene un objeto primario, así como un campo de datos que el objeto primario usa para almacenar información específica sobre este objeto, por ejemplo, su posición. Este objeto es responsable de la implementación de los protocolos básicos de representación y diseño.

RenderObject no limita el modelo de uso de objetos secundarios: puede haber ninguno, uno o muchos. Además, el sistema de posicionamiento no se limita a: el sistema cartesiano, las coordenadas polares, todo esto y mucho más está disponible para su uso. No hay restricciones en el uso de protocolos de ubicación: ajuste del ancho o alto, limitación del tamaño, configuración del tamaño y ubicación del elemento primario o, si es necesario, uso de los datos del objeto principal.

Flutter World Picture


Tratemos de construir una imagen general de cómo funciona todo junto.

Ya señalamos anteriormente, el widget es una descripción inmutable, pero la interfaz de usuario no es estática. Esta discrepancia se elimina dividiendo en 3 niveles de objetos y la división de zonas de responsabilidad.

  • , .
  • , .
  • , — , .

imagen

Veamos cómo se ven estos árboles con un ejemplo simple:

imagen

en este caso, tenemos un StatelessWidget envuelto en un widget de Relleno y que contiene texto dentro.

Pongámonos en lugar de Flutter: nos dieron este árbol de widgets.

Flutter: "Hey, Padding, necesito tu elemento"
Padding: "Por supuesto, mantén SingleChildRenderObjectElement"

imagen

Flutter: "Element, aquí está tu lugar, establecete"
SingleChildRenderObjectElement: "Chicos, todo está bien, pero necesito RenderObject"
Flutter: "Padding, como para dibujarte?
Relleno: "Hold it, RenderPadding"
SingleChildRenderObjectElement: "Genial, ponte a trabajar"

imagen

Flutter:"Entonces, ¿quién es el próximo?" StatelessWidget, ahora deja que el elemento »
StatelessWidget: «Aquí StatelessElement»
trémolo: «StatelessElement, que estará en sujeción a SingleChildRenderObjectElement, aquí está el lugar, embarcarse»
StatelessElement: «OK»

imagen

trémolo: «el texto enriquecido, presente elementik, por favor»
del texto enriquecido da MultiChildRenderObjectElement
trémolo: "MultiChildRenderObjectElement, aquí tienes, comienza"
MultiChildRenderObjectElement: "Necesito un render para trabajar"
Flutter: "RichText, necesitamos un objeto de render"
RichText: "Aquí hay un RenderParagraph"
Flutter:"RenderParagraph recibirás instrucciones RenderPadding, y controlarás MultiChildRenderObjectElement"
MultiChildRenderObjectElement: "Ahora todo está bien, estoy listo"

imagen

Seguramente harás una pregunta legítima: "¿Dónde está el objeto de representación para StatelessWidget? ¿Por qué no está allí? Decidimos que los elementos unen configuraciones con pantalla? " Prestemos atención a la implementación básica del método de montaje, que se discutió en esta sección de la descripción del ciclo de vida.

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;
    }());
}

No veremos en él la creación de un objeto de representación. Pero el elemento implementa un BuildContext, que tiene un método de búsqueda de objetos de visualización findRenderObject, que nos llevará al siguiente getter:

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;
}

En el caso básico, un elemento puede no crear un objeto de representación; solo RenderObjectElement y sus descendientes están obligados a hacerlo; sin embargo, en este caso, un elemento en algún nivel de anidación debe tener un elemento secundario que tenga un objeto de representación.

Parecería por qué todas estas dificultades. Hasta 3 árboles, diferentes áreas de responsabilidad, etc. La respuesta es bastante simple: aquí es donde se construye el rendimiento de Flutter. Los widgets son configuraciones inmutables, por lo tanto, a menudo se recrean, pero al mismo tiempo son bastante livianos, lo que no afecta el rendimiento. Pero Flutter está tratando de reutilizar elementos pesados ​​tanto como sea posible.

Considera un ejemplo.

Texto en el medio de la pantalla. El código en este caso se verá así:

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

En este caso, el árbol de widgets se verá así:

imagen

Después de que Flutter construya los 3 árboles, obtenemos la siguiente imagen:

imagen

¿Qué sucede si cambiamos el texto que vamos a mostrar?

imagen

Ahora tenemos un nuevo árbol de widgets. Arriba hablamos sobre la máxima reutilización posible de elementos. Eche un vistazo al método de clase Widget, bajo el nombre parlante canUpdate .

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

Verificamos el tipo del widget anterior y el nuevo, así como sus claves. Si son iguales, entonces no hay necesidad de cambiar el artículo.

Entonces, antes de la actualización, el primer elemento es Centro, después de la actualización, también Centro. Ambos no tienen llaves, una completa coincidencia. Podemos actualizar el enlace del elemento a un nuevo widget.

imagen

Pero además del tipo y la clave, el widget es una descripción y configuración, y los valores de los parámetros necesarios para la visualización podrían cambiar. Es por eso que el elemento, después de actualizar el enlace al widget, debe iniciar actualizaciones para el objeto de representación. En el caso de Center, nada ha cambiado y seguimos comparando más.

Una vez más, el tipo y la clave nos dicen que no tiene sentido recrear el elemento. El texto es un descendiente de StatelessWidget; no tiene un objeto de visualización directa.

imagen

Vaya a RichText. El widget tampoco ha cambiado su tipo; no hay discrepancias en las claves. El elemento actualiza su asociación con el nuevo widget.

imagen

La conexión se actualiza, solo queda actualizar las propiedades. Como resultado, RenderParagraph mostrará el nuevo valor de texto.

imagen

Y tan pronto como llegue el momento del próximo marco de dibujo, veremos el resultado que esperamos.

Gracias a este tipo de trabajo, Flutter logra un rendimiento tan alto.

El ejemplo anterior describe el caso cuando la estructura del widget en sí no ha cambiado. Pero, ¿qué pasa si la estructura cambia? Flutter, por supuesto, continuará tratando de maximizar el uso de los objetos existentes, como entendimos por la descripción del ciclo de vida, pero se crearán nuevos elementos para todos los nuevos widgets, y los antiguos y más innecesarios se eliminarán al final del marco.

Veamos un par de ejemplos. Y para asegurarnos de lo anterior, utilizamos la herramienta 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,
        ),
    ],
);

En este caso, al hacer clic en el botón, uno de los widgets cambiará. Veamos qué nos muestra el inspector.

imagen

imagen

Como podemos ver, Flutter recreó el render solo para Padding, el resto simplemente se reutilizó.

Considere 1 opción más en la que la estructura cambia de una manera más global: cambiamos los niveles de anidamiento.

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

imagen

imagen

A pesar de que el árbol no cambió visualmente en absoluto, se recrearon los elementos y objetos del árbol de representación. Esto sucedió porque Flutter compara por nivel (en este caso, no importa que la mayor parte del árbol no haya cambiado), el tamizado de esta parte tuvo lugar al momento de comparar Contenedor y Fila. Sin embargo, uno puede salir de esta situación. Esto nos ayudará a GlobalKey. Agregue una clave para Row.

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

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

imagen

imagen

Tan pronto como le dijimos a Flutter que la pieza podía reutilizarse, aprovechó la oportunidad.

Conclusión


Nos acercamos un poco más a la magia Flutter y ahora sabemos que no solo se trata de widgets.

Flutter es un mecanismo bien coordinado y bien pensado con su propia jerarquía, áreas de responsabilidad, con el que puede crear no solo aplicaciones hermosas, sino también productivas. Por supuesto, hemos examinado solo una pequeña parte, aunque bastante importante, de su dispositivo, por lo que continuaremos analizando varios aspectos del funcionamiento interno del marco en futuros artículos.

Espero que la información en este artículo sea útil para comprender cómo Flutter funciona internamente y lo ayude a encontrar soluciones elegantes y productivas durante el desarrollo.

¡Gracias por la atención!

Recursos


Flutter
"Cómo Flutter hace Widgets" por Andrew Fitz Gibbon, Matt Sullivan

All Articles