Dentro de Flutter
Obtén información sobre el funcionamiento interno de Flutter de la mano de uno de sus ingenieros fundadores.
Este documento describe el funcionamiento interno del toolkit de Flutter que hace posible la API de Flutter. Dado que los widgets de Flutter se construyen usando una composición agresiva, las interfaces de usuario construidas con Flutter tienen una gran cantidad de widgets. Para soportar esta carga de trabajo, Flutter utiliza algoritmos sublineales para el layout y la construcción de widgets, así como estructuras de datos que hacen que la cirugía de árboles sea eficiente y que cuentan con varias optimizaciones de factor constante. Con algunos detalles adicionales, este diseño también facilita a los desarrolladores crear listas de desplazamiento infinito utilizando callbacks que construyen exactamente aquellos widgets que son visibles para el usuario.
Composabilidad agresiva
#Uno de los aspectos más distintivos de Flutter es su composabilidad
agresiva. Los Widgets se construyen componiendo otros widgets,
que a su vez están construidos a partir de widgets progresivamente más básicos.
Por ejemplo, Padding es un widget en lugar de una propiedad de otros widgets.
Como resultado, las interfaces de usuario construidas con Flutter consisten en muchos,
muchísimos widgets.
La recursión en la construcción de widgets toca fondo en los RenderObjectWidgets,
que son widgets que crean nodos en el árbol de render subyacente.
El árbol de render es una estructura de datos que almacena la geometría de la interfaz
de usuario, la cual se calcula durante el layout y se utiliza durante el pintado
y el
hit testing. La mayoría de los desarrolladores de Flutter no crean render objects directamente
sino que manipulan el árbol de render utilizando widgets.
Para soportar una composabilidad agresiva en la capa de widgets, Flutter utiliza una serie de algoritmos eficientes y optimizaciones tanto en la capa de widgets como en la del árbol de render, los cuales se describen en las siguientes subsecciones.
Layout sublineal
#Con una gran cantidad de widgets y render objects, la clave para un buen rendimiento son los algoritmos eficientes. De primordial importancia es el rendimiento del layout, que es el algoritmo que determina la geometría (por ejemplo, el tamaño y la posición) de los render objects. Algunos otros toolkits utilizan algoritmos de layout que son O(N²) o peor (por ejemplo, iteración de punto fijo en algún dominio de restricciones). Flutter apunta a un rendimiento lineal para el layout inicial, y a un rendimiento de layout sublineal en el caso común de actualizar posteriormente un layout existente. Típicamente, la cantidad de tiempo empleada en el layout debería escalar más lentamente que el número de render objects.
Flutter realiza un layout por fotograma (frame), y el algoritmo de layout funciona en una sola pasada. Las restricciones se pasan hacia abajo en el árbol mediante objetos padre que llaman al método de layout en cada uno de sus hijos. Los hijos realizan recursivamente su propio layout y luego devuelven la geometría hacia arriba en el árbol retornando de su método de layout. Es importante destacar que una vez que un render object ha retornado de su método de layout, ese render object no volverá a ser visitado1 hasta el layout del siguiente fotograma. Este enfoque combina lo que de otro modo serían pasadas separadas de medición y layout en una sola pasada y, como resultado, cada render object es visitado como máximo dos veces2 durante el layout: una vez de bajada en el árbol, y una vez de subida en el árbol.
Flutter tiene varias especializaciones de este protocolo general.
La especialización más común es RenderBox, que opera en
coordenadas cartesianas bidimensionales. En el layout de caja (box layout), las restricciones
son un ancho mínimo y máximo y un alto mínimo y máximo. Durante el layout,
el hijo determina su geometría eligiendo un tamaño dentro de estos límites.
Después de que el hijo retorna del layout, el padre decide la posición del hijo
en el sistema de coordenadas del padre3.
Ten en cuenta que el layout del hijo no puede depender de su posición,
ya que la posición no se determina hasta después de que el hijo
retorne del layout. Como resultado, el padre es libre de reposicionar
al hijo sin necesidad de volver a calcular su layout.
Más generalmente, durante el layout, la única información que fluye de padre a hijo son las restricciones y la única información que fluye de hijo a padre es la geometría. Estos invariantes pueden reducir la cantidad de trabajo requerido durante el layout:
-
Si el hijo no ha marcado su propio layout como sucio (dirty), el hijo puede retornar inmediatamente del layout, interrumpiendo el recorrido, siempre y cuando el padre le dé al hijo las mismas restricciones que el hijo recibió durante el layout anterior.
-
Cada vez que un padre llama al método de layout de un hijo, el padre indica si utiliza la información de tamaño devuelta por el hijo. Si, como sucede a menudo, el padre no utiliza la información de tamaño, entonces el padre no necesita volver a calcular su layout si el hijo selecciona un nuevo tamaño porque el padre tiene la garantía de que el nuevo tamaño se ajustará a las restricciones existentes.
-
Las restricciones estrictas (tight) son aquellas que se pueden satisfacer mediante exactamente una geometría válida. Por ejemplo, si el ancho mínimo y máximo son iguales entre sí y el alto mínimo y máximo son iguales entre sí, el único tamaño que satisface esas restricciones es uno con ese ancho y alto. Si el padre proporciona restricciones estrictas, entonces el padre no necesita volver a calcular su layout cada vez que el hijo vuelve a calcular su layout, incluso si el padre utiliza el tamaño del hijo en su layout, porque el hijo no puede cambiar de tamaño sin nuevas restricciones de su padre.
-
Un render object puede declarar que utiliza las restricciones proporcionadas por el padre solo para determinar su geometría. Dicha declaración informa al framework que el padre de ese render object no necesita volver a calcular su layout cuando el hijo vuelve a calcular su layout incluso si las restricciones no son estrictas e incluso si el layout del padre depende del tamaño del hijo, porque el hijo no puede cambiar de tamaño sin nuevas restricciones de su padre.
Como resultado de estas optimizaciones, cuando el árbol de render objects contiene nodos sucios (dirty), solo esos nodos y una parte limitada del subárbol a su alrededor se visitan durante el layout.
Construcción de widgets sublineal
#De manera similar al algoritmo de layout, el algoritmo de construcción de widgets de Flutter es sublineal. Después de ser construidos, los widgets son mantenidos por el árbol de elementos (element tree), que conserva la estructura lógica de la interfaz de usuario. El árbol de elementos es necesario porque los widgets en sí son inmutables, lo que significa (entre otras cosas) que no pueden recordar sus relaciones de padre o hijo con otros widgets. El árbol de elementos también mantiene los objetos de State asociados con los widgets Stateful.
En respuesta a las entradas del usuario (u otros estímulos), un elemento se puede volver sucio (dirty),
por ejemplo si el desarrollador llama a setState() en el objeto State
asociado. El framework mantiene una lista de elementos sucios y salta directamente
a ellos durante la fase de build, omitiendo los elementos limpios. Durante
la fase de build, la información fluye unidireccionalmente hacia abajo en el árbol
de elementos, lo que significa que cada elemento se visita como máximo una vez durante la fase de build.
Una vez limpio, un elemento no puede volverse sucio de nuevo porque,
por inducción, todos sus elementos ancestros también están limpios4.
Debido a que los widgets son inmutables, si un elemento no se ha marcado como sucio, el elemento puede retornar inmediatamente de build, interrumpiendo el recorrido, si el padre vuelve a construir el elemento con un widget idéntico. Además, el elemento solo necesita comparar la identidad del objeto de las dos referencias de widget para establecer que el nuevo widget es el mismo que el widget antiguo. Los desarrolladores aprovechan esta optimización para implementar el patrón de reproyección, en el cual un widget incluye un widget hijo preconstruido almacenado como una variable miembro en su build.
Durante build, Flutter también evita recorrer la cadena de padres utilizando
InheritedWidgets. Si los widgets recorrieran comúnmente su cadena de padres,
por ejemplo para determinar el color del tema actual, la fase de build
se volvería O(N²) en la profundidad del árbol, la cual puede ser bastante
grande debido a la composabilidad agresiva. Para evitar estos recorridos hacia los padres,
el framework empuja la información hacia abajo en el árbol de elementos manteniendo
una tabla hash de InheritedWidgets en cada elemento. Típicamente, muchos
elementos referenciarán la misma tabla hash, la cual cambia solo en
los elementos que introducen un nuevo InheritedWidget.
Reconciliación lineal
#Contrariamente a la creencia popular, Flutter no emplea un algoritmo de diferenciación de árboles (tree-diffing). En su lugar, el framework decide si reutilizar elementos examinando la lista de hijos de cada elemento de forma independiente utilizando un algoritmo O(N). El algoritmo de reconciliación de la lista de hijos se optimiza para los siguientes casos:
- La lista de hijos antigua está vacía.
- Las dos listas son idénticas.
- Hay una inserción o eliminación de uno o más widgets en exactamente un lugar de la lista.
- Si cada lista contiene un widget con la misma key5, los dos widgets se coinciden (match).
El enfoque general consiste en hacer coincidir el principio y el final de ambas listas de hijos comparando el tipo en tiempo de ejecución (runtime type) y la key de cada widget, encontrando potencialmente un rango no vacío en el medio de cada lista que contiene todos los hijos no coincidentes. Luego, el framework coloca los hijos dentro del rango de la lista antigua de hijos en una tabla hash basada en sus keys. A continuación, el framework recorre el rango en la nueva lista de hijos y consulta la tabla hash por key en busca de coincidencias. Los hijos no coincidentes se descartan y se vuelven a construir desde cero, mientras que los hijos coincidentes se vuelven a construir con sus nuevos widgets.
Cirugía de árbol
#Reutilizar elementos es importante para el rendimiento porque los elementos poseen dos piezas críticas de datos: el state para los widgets Stateful y los render objects subyacentes. Cuando el framework puede reutilizar un elemento, el state de esa parte lógica de la interfaz de usuario se conserva y la información de layout calculada previamente se puede reutilizar, evitando a menudo recorridos completos del subárbol. De hecho, reutilizar elementos es tan valioso que Flutter admite mutaciones de árbol no locales que conservan la información de state y layout.
Los desarrolladores pueden realizar una mutación de árbol no local asociando un GlobalKey
con uno de sus widgets. Cada global key es única en toda la
aplicación y está registrada en una tabla hash específica del hilo (thread).
Durante la fase de build, el desarrollador puede mover un widget con una global
key a una ubicación arbitraria en el árbol de elementos. En lugar de construir
un elemento nuevo en esa ubicación, el framework consultará la tabla
hash y reasignará el padre (reparent) del elemento existente desde su ubicación anterior a
su nueva ubicación, conservando todo el subárbol.
Los render objects en el subárbol reasignado pueden conservar su información de layout porque las restricciones de layout son la única información que fluye de padre a hijo en el árbol de render. El nuevo padre se marca como sucio (dirty) para el layout porque su lista de hijos ha cambiado, pero si el nuevo padre pasa al hijo las mismas restricciones de layout que el hijo recibió de su padre anterior, el hijo puede retornar inmediatamente del layout, interrumpiendo el recorrido.
Las global keys y las mutaciones de árbol no locales son utilizadas extensamente por los desarrolladores para lograr efectos como transiciones hero y navegación.
Optimizaciones de factor constante
#Además de estas optimizaciones algorítmicas, lograr una composabilidad agresiva también se basa en varias optimizaciones importantes de factor constante. Estas optimizaciones son más importantes en las hojas de los algoritmos principales discutidos anteriormente.
-
Agnóstico del modelo de hijos. A diferencia de la mayoría de los toolkits, que utilizan listas de hijos, el árbol de render de Flutter no se compromete con un modelo de hijos específico. Por ejemplo, la clase
RenderBoxtiene un método abstractovisitChildren()en lugar de una interfaz concreta defirstChildynextSibling. Muchas subclases admiten solo un único hijo, mantenido directamente como una variable miembro, en lugar de una lista de hijos. Por ejemplo,RenderPaddingadmite solo un único hijo y, como resultado, tiene un método de layout más simple que requiere menos tiempo para ejecutarse. -
Árbol de render visual, árbol de widgets lógico. En Flutter, el árbol de render opera en un sistema de coordenadas visuales e independiente del dispositivo, lo que significa que valores más pequeños en la coordenada x están siempre hacia la izquierda, incluso si la dirección de lectura actual es de derecha a izquierda. El árbol de widgets típicamente opera en coordenadas lógicas, lo que significa con valores de start y end cuya interpretación visual depende de la dirección de lectura. La transformación de coordenadas lógicas a visuales se realiza en la transferencia entre el árbol de widgets y el árbol de render. Este enfoque es más eficiente porque los cálculos de layout y pintado en el árbol de render ocurren con más frecuencia que la transferencia del árbol de widgets al de render y se pueden evitar conversiones de coordenadas repetidas.
-
Texto manejado por un render object especializado. La gran mayoría de los render objects ignoran las complejidades del texto. En su lugar, el texto es manejado por un render object especializado,
RenderParagraph, que es una hoja en el árbol de render. En lugar de crear subclases de un render object consciente del texto, los desarrolladores incorporan texto en su interfaz de usuario mediante composición. Este patrón significa queRenderParagraphpuede evitar volver a calcular su layout de texto mientras su padre proporcione las mismas restricciones de layout, lo cual es común, incluso durante la cirugía de árbol. -
Objetos observables. Flutter utiliza tanto el paradigma de modelo-observación como el paradigma reactivo. Obviamente, el paradigma reactivo es el dominante, pero Flutter utiliza objetos de modelo observables para algunas estructuras de datos hoja. Por ejemplo, las
Animations notifican a una lista de observadores cuando cambia su valor. Flutter transfiere estos objetos observables desde el árbol de widgets al árbol de render, que los observa directamente e invalida solo la etapa apropiada del pipeline cuando cambian. Por ejemplo, un cambio en unAnimation<Color>podría activar solo la fase de pintado en lugar de ambas fases de build y pintado.
Tomadas en conjunto y sumadas sobre los grandes árboles creados por una composición agresiva, estas optimizaciones tienen un efecto sustancial en el rendimiento.
Separación de los árboles Element y RenderObject
#Los árboles de RenderObject y Element (Widget) en Flutter son isomórficos
(estrictamente hablando, el árbol de RenderObject es un subconjunto del árbol de Element).
Una simplificación obvia sería combinar estos árboles en
un solo árbol. Sin embargo, en la práctica hay una serie de beneficios al tener
estos árboles separados:
-
Rendimiento. Cuando cambia el layout, solo es necesario recorrer las partes relevantes del árbol de layout. Debido a la composición, el árbol de elementos con frecuencia tiene muchos nodos adicionales que tendrían que ser omitidos.
-
Claridad. La separación más clara de responsabilidades permite que el protocolo de widgets y el protocolo de render objects se especialicen cada uno en sus necesidades específicas, simplificando la superficie de la API y reduciendo así el riesgo de errores y la carga de pruebas.
-
Seguridad de tipos. El árbol de render objects puede ser más seguro en cuanto a tipos, ya que puede garantizar en tiempo de ejecución que los hijos serán del tipo apropiado (cada sistema de coordenadas, por ejemplo, tiene su propio tipo de render object). Los widgets de composición pueden ser agnósticos sobre el sistema de coordenadas utilizado durante el layout (por ejemplo, el mismo widget que expone una parte del modelo de la app podría usarse tanto en un layout de caja como en un layout sliver) y, por lo tanto, en el árbol de elementos, verificar el tipo de los render objects requeriría un recorrido del árbol.
Desplazamiento infinito
#Las listas de desplazamiento infinito son notoriamente difíciles para los toolkits.
Flutter admite listas de desplazamiento infinito con una interfaz simple
basada en el patrón builder, en la cual un ListView utiliza un callback
para construir widgets bajo demanda a medida que se vuelven visibles para el usuario durante el
desplazamiento. Soportar esta característica requiere un layout orientado al viewport
y la construcción de widgets bajo demanda.
Layout orientado al Viewport
#Como la mayoría de las cosas en Flutter, los widgets desplazables se construyen utilizando
composición. El exterior de un widget desplazable es un Viewport,
que es una caja que es "más grande por dentro", lo que significa que sus hijos
pueden extenderse más allá de los límites del viewport y se pueden desplazar para mostrarse en
vista. Sin embargo, en lugar de tener hijos RenderBox, un viewport tiene
hijos RenderSliver, conocidos como slivers, los cuales tienen un protocolo
de layout orientado al viewport.
El protocolo de layout de sliver coincide con la estructura del protocolo de layout de caja (box layout) en que los padres pasan restricciones a sus hijos y reciben geometría a cambio. Sin embargo, los datos de restricciones y geometría difieren entre los dos protocolos. En el protocolo sliver, a los hijos se les da información sobre el viewport, incluyendo la cantidad de espacio visible restante. Los datos de geometría que devuelven permiten una variedad de efectos vinculados al desplazamiento, incluyendo encabezados colapsables y paralaje.
Diferentes slivers llenan el espacio disponible en el viewport de diferentes maneras. Por ejemplo, un sliver que produce una lista lineal de hijos ubica (lays out) cada hijo en orden hasta que el sliver se queda sin hijos o se queda sin espacio. De manera similar, un sliver que produce una cuadrícula bidimensional de hijos llena solo la porción de su cuadrícula que es visible. Debido a que son conscientes de cuánto espacio es visible, los slivers pueden producir un número finito de hijos incluso si tienen el potencial de producir un número ilimitado de hijos.
Los slivers se pueden componer para crear layouts y efectos desplazables personalizados. Por ejemplo, un solo viewport puede tener un encabezado colapsable seguido de una lista lineal y luego una cuadrícula. Los tres slivers cooperarán a través del protocolo de layout de sliver para producir solo aquellos hijos que son realmente visibles a través del viewport, independientemente de si esos hijos pertenecen al encabezado, a la lista o a la cuadrícula6.
Construcción de widgets bajo demanda
#Si Flutter tuviera un pipeline estricto de construcción-luego-layout-luego-pintado, lo anterior sería insuficiente para implementar una lista de desplazamiento infinito porque la información sobre cuánto espacio es visible a través del viewport está disponible solo durante la fase de layout. Sin mecanismos adicionales, la fase de layout es demasiado tarde para construir los widgets necesarios para llenar el espacio. Flutter resuelve este problema intercalando las fases de construcción (build) y layout del pipeline. En cualquier momento de la fase de layout, el framework puede comenzar a construir nuevos widgets bajo demanda siempre que esos widgets sean descendientes del render object que realiza el layout en ese momento.
Intercalar construcción y layout es posible solo debido a los estrictos controles sobre la propagación de información en los algoritmos de construcción y layout. Específicamente, durante la fase de construcción, la información puede propagarse solo hacia abajo en el árbol. Cuando un render object está realizando el layout, el recorrido de layout no ha visitado el subárbol debajo de ese render object, lo que significa que las escrituras generadas al construir en ese subárbol no pueden invalidar ninguna información que haya entrado en el cálculo de layout hasta el momento. De manera similar, una vez que el layout ha retornado de un render object, ese render object nunca será visitado de nuevo durante este layout, lo que significa que cualquier escritura generada por los cálculos de layout posteriores no puede invalidar la información utilizada para construir el subárbol del render object.
Además, la reconciliación lineal y la cirugía de árbol son esenciales para actualizar eficientemente los elementos durante el desplazamiento y para modificar el árbol de render cuando los elementos entran y salen de la vista en el borde del viewport.
Ergonomía de la API
#Ser rápido solo importa si el framework realmente se puede utilizar de manera efectiva. Para guiar el diseño de la API de Flutter hacia una mayor usabilidad, Flutter ha sido probado repetidamente en extensos estudios de UX con desarrolladores. Estos estudios a veces confirmaron decisiones de diseño preexistentes, a veces ayudaron a guiar la priorización de características y a veces cambiaron la dirección del diseño de la API. Por ejemplo, las APIs de Flutter están fuertemente documentadas; los estudios de UX confirmaron el valor de dicha documentación, pero también destacaron la necesidad específica de código de ejemplo y diagramas ilustrativos.
Esta sección discute algunas de las decisiones tomadas en el diseño de la API de Flutter para favorecer la usabilidad.
Especializando APIs para adaptarse a la mentalidad del desarrollador
#La clase base para los nodos en los árboles de Widget, Element y RenderObject
de Flutter no define un modelo de hijos. Esto permite que cada nodo se
especialice para el modelo de hijos que sea aplicable a ese nodo.
La mayoría de los objetos Widget tienen un solo Widget hijo y, por lo tanto, solo exponen
un único parámetro child. Algunos widgets admiten una cantidad arbitraria de
hijos y exponen un parámetro children que toma una lista.
Algunos widgets no tienen ningún hijo y no reservan memoria,
y no tienen parámetros para ellos. De manera similar, los RenderObjects exponen APIs
específicas para su modelo de hijos. RenderImage es un nodo hoja y no tiene
concepto de hijos. RenderPadding toma un solo hijo, por lo que tiene almacenamiento
para un solo puntero a un solo hijo. RenderFlex toma un número arbitrario
de hijos y los gestiona como una lista enlazada.
En algunos casos raros, se utilizan modelos de hijos más complicados. El
constructor del render object RenderTable toma una matriz de matrices de
hijos, la clase expone getters y setters que controlan el número
de filas y columnas, y existen métodos específicos para reemplazar
hijos individuales por coordenadas x,y, para agregar una fila, para proporcionar una
nueva matriz de matrices de hijos y para reemplazar la lista completa de hijos
con una sola matriz y un recuento de columnas. En la implementación,
el objeto no utiliza una lista enlazada como la mayoría de los render objects, sino que
utiliza un arreglo indexable.
Los widgets Chip y los objetos InputDecoration tienen campos que coinciden
con los slots existentes en los controles correspondientes. Donde un modelo de hijos único para todo
forzaría a superponer semántica sobre una lista de hijos, por ejemplo,
definiendo que el primer hijo sea el valor de prefijo y el segundo el sufijo, el modelo de hijos dedicado
permite utilizar propiedades nombradas dedicadas en su lugar.
Esta flexibilidad permite que cada nodo en estos árboles se manipule de la forma más idiomática para su función. Es raro querer insertar una celda en una tabla haciendo que todas las demás celdas se ajusten alrededor; de manera similar, es raro querer eliminar un hijo de una fila flex por índice en lugar de por referencia.
El objeto RenderParagraph es el caso más extremo: tiene un hijo de
un tipo completamente diferente, TextSpan. En el límite de RenderParagraph,
el árbol de RenderObject hace la transición a ser un árbol de TextSpan.
El enfoque general de especializar las APIs para cumplir con las expectativas del desarrollador se aplica a más cosas que solo a los modelos de hijos.
Existen algunos widgets bastante triviales específicamente para que los desarrolladores
los encuentren al buscar una solución a un problema. Agregar un
espacio a una fila o columna se hace fácilmente una vez que uno sabe cómo, utilizando
el widget Expanded y un hijo SizedBox de tamaño cero, pero descubrir
ese patrón es innecesario porque buscar space
revela el widget Spacer, que utiliza Expanded y SizedBox
directamente
para lograr el efecto.
De manera similar, ocultar un subárbol de widgets se hace fácilmente al no incluir el
subárbol de widgets en el build en absoluto. Sin embargo, los desarrolladores típicamente esperan
que haya un widget para hacer esto, por lo que el widget Visibility existe
para envolver este patrón en un widget reutilizable trivial.
Argumentos explícitos
#Las frameworks de UI tienden a tener muchas propiedades, de tal modo que un desarrollador rara vez es capaz de recordar el significado semántico de cada argumento del constructor de cada clase. Como Flutter utiliza el paradigma reactivo, es común que los métodos build en Flutter tengan muchas llamadas a constructores. Aprovechando el soporte de Dart para argumentos nombrados, la API de Flutter puede mantener dichos métodos build claros y comprensibles.
Este patrón se extiende a cualquier método con múltiples argumentos,
y en particular se extiende a cualquier argumento booleano, de modo que los literales
true o false aislados en las llamadas a métodos sean siempre autodocumentados.
Además, para evitar la confusión comúnmente causada por las dobles negativas
en las APIs, los argumentos y propiedades booleanos siempre se nombran en
forma positiva (por ejemplo, enabled: true en lugar de disabled: false).
Allanando trampas
#Una técnica utilizada en varios lugares en el framework de Flutter es definir la API de manera que las condiciones de error no existan. Esto elimina clases enteras de errores de la consideración.
Por ejemplo, las funciones de interpolación permiten que uno o ambos extremos de la interpolación sean nulos (null), en lugar de definir eso como un caso de error: interpolar entre dos valores nulos es siempre nulo, e interpolar desde un valor nulo o hacia un valor nulo es el equivalente a interpolar hacia el análogo a cero para el tipo dado. Esto significa que los desarrolladores que accidentalmente pasen null a una función de interpolación no encontrarán un caso de error, sino que obtendrán un resultado razonable.
Un ejemplo más sutil está en el algoritmo de layout de Flex. El concepto de
este layout es que el espacio otorgado al render object flex se
divide entre sus hijos, por lo que el tamaño del flex debería ser la
totalidad del espacio disponible. En el diseño original, proporcionar
espacio infinito fallaría: implicaría que el flex debería tener un tamaño
infinito, una configuración de layout inútil. En su lugar, la API
se ajustó de modo que cuando se asigna espacio infinito al render object
flex, el render object se dimensiona a sí mismo para ajustarse al tamaño deseado
de los hijos, reduciendo la cantidad posible de casos de error.
El enfoque también se utiliza para evitar tener constructores que permitan
crear datos inconsistentes. Por ejemplo, el constructor de PointerDownEvent
no permite que la propiedad down de PointerEvent
se
establezca en false (una situación que sería autocontradictoria);
en su lugar, el constructor no tiene un parámetro para el campo down
y siempre lo establece en true.
En general, el enfoque es definir interpretaciones válidas para todos los
valores en el dominio de entrada. El ejemplo más simple es el constructor de Color.
En lugar de tomar cuatro enteros, uno para el rojo, uno para el verde,
uno para el azul y uno para la alfa, cada uno de los cuales podría estar fuera de rango,
el constructor por defecto toma un solo valor entero, y define
el significado de cada bit (por ejemplo, los ocho bits inferiores definen la
componente roja), de modo que cualquier valor de entrada sea un valor de color válido.
Un ejemplo más elaborado es la función paintImage(). Esta función
toma once argumentos, algunos con dominios de entrada bastante amplios, pero
han sido cuidadosamente diseñados para ser mayormente ortogonales entre sí,
de tal manera que hay muy pocas combinaciones inválidas.
Reportando casos de error de forma agresiva
#No todas las condiciones de error se pueden eliminar mediante el diseño. Para las que permanecen, en las builds de depuración (debug builds), Flutter generalmente intenta capturar los errores muy temprano y los reporta inmediatamente. Las aserciones (asserts) se utilizan ampliamente. Los argumentos del constructor se verifican minuciosamente. Los ciclos de vida se monitorean y cuando se detectan inconsistencias inmediatamente provocan que se lance una excepción.
En algunos casos, esto se lleva a extremos: por ejemplo, al ejecutar
pruebas unitarias, independientemente de lo que esté haciendo la prueba, cada subclase de RenderBox
a la que se le realiza el layout inspecciona agresivamente si sus métodos de
dimensionamiento intrínseco cumplen con el contrato de dimensionamiento intrínseco. Esto ayuda a capturar
errores en APIs que de otro modo podrían no ser ejercitadas.
Cuando se lanzan excepciones, incluyen tanta información como esté disponible. Algunos de los mensajes de error de Flutter sondean proactivamente el rastreo de la pila (stack trace) asociado para determinar la ubicación más probable del error real. Otros recorren los árboles relevantes para determinar la fuente de los datos incorrectos. Los errores más comunes incluyen instrucciones detalladas incluyendo en algunos casos código de muestra para evitar el error, o enlaces a documentación adicional.
Paradigma reactivo
#Las APIs mutables basadas en árboles sufren de un patrón de acceso dicotómico: crear el estado original del árbol típicamente utiliza un conjunto de operaciones muy diferente al de las actualizaciones posteriores. La capa de renderizado de Flutter utiliza este paradigma, ya que es una forma efectiva de mantener un árbol persistente, lo cual es clave para un layout y pintado eficientes. Sin embargo, significa que la interacción directa con la capa de renderizado es incómoda en el mejor de los casos y propensa a errores en el peor.
La capa de widgets de Flutter introduce un mecanismo de composición utilizando el paradigma reactivo7 para manipular el árbol de renderizado subyacente. Esta API abstrae la manipulación del árbol combinando los pasos de creación del árbol y mutación del árbol en un solo paso de descripción del árbol (build), donde, después de cada cambio en el estado del sistema, el desarrollador describe la nueva configuración de la interfaz de usuario y el framework calcula la serie de mutaciones del árbol necesarias para reflejar esta nueva configuración.
Interpolación
#Dado que el framework de Flutter anima a los desarrolladores a describir la configuración de la interfaz que coincida con el estado actual de la aplicación, existe un mecanismo para animar implícitamente entre estas configuraciones.
Por ejemplo, supón que en el estado S1 la interfaz consiste en un círculo, pero en el estado S2 consiste en un cuadrado. Sin un mecanismo de animación, el cambio de estado tendría un cambio de interfaz brusco. Una animación implícita permite que el círculo se transforme suavemente en un cuadrado a lo largo de varios fotogramas.
Cada característica que se puede animar implícitamente tiene un widget Stateful que mantiene un registro del valor actual de la entrada y comienza una secuencia de animación cada vez que cambia el valor de entrada, haciendo la transición desde el valor actual al nuevo valor durante una duración especificada.
Esto se implementa usando funciones lerp (interpolación lineal) utilizando
objetos inmutables. Cada estado (círculo y cuadrado, en este caso)
se representa como un objeto inmutable que se configura con los
ajustes apropiados (color, ancho de trazo, etc.) y sabe cómo pintarse
a sí mismo. Cuando llega el momento de dibujar los pasos intermedios durante la animación,
los valores inicial y final se pasan a la función lerp adecuada
junto con un valor t que representa el punto a lo largo de la animación,
donde 0.0 representa el inicio (start) y 1.0 representa el fin (end)8,
y la función devuelve un tercer objeto inmutable que representa la
etapa intermedia.
Para la transición de círculo a cuadrado, la función lerp devolvería
un objeto que representa un "cuadrado redondeado" con un radio descrito como
una fracción derivada del valor t, un color interpolado utilizando la
función lerp para colores, y un ancho de trazo interpolado utilizando la
función lerp para doubles. Ese objeto, que implementa la
misma interfaz que los círculos y los cuadrados, sería capaz de pintarse
a sí mismo cuando se le solicite.
Esta técnica permite que la maquinaria de estados, el mapeo de estados a configuraciones, la maquinaria de animación, la maquinaria de interpolación, y la lógica específica referente a cómo pintar cada fotograma estén completamente separadas entre sí.
Este enfoque es de amplia aplicación. En Flutter, los tipos básicos como
Color y Shape se pueden interpolar, pero también tipos mucho
más elaborados como Decoration, TextStyle o Theme. Estos
típicamente se construyen a partir de componentes que a su vez se pueden interpolar,
e interpolar los objetos más complicados a menudo es tan simple como
interpolar recursivamente todos los valores que describen los objetos complicados.
Algunos objetos interpolables se definen mediante jerarquías de clases. Por ejemplo,
las formas (shapes) están representadas por la interfaz ShapeBorder, y existe una
variedad de formas, incluyendo BeveledRectangleBorder, BoxBorder,
CircleBorder, RoundedRectangleBorder y StadiumBorder. Una sola
función lerp no puede anticipar todos los tipos posibles,
y por lo tanto la interfaz define en su lugar los métodos lerpFrom y lerpTo,
a los que se delega el método estático lerp. Cuando se le indica interpolar desde
una forma A hacia una forma B, primero se le pregunta a B si puede hacer lerpFrom A, luego,
si no puede, se le pregunta a A si puede hacer lerpTo B. (Si ninguno es
posible, la función devuelve A para valores de t menores a 0.5,
y devuelve B en caso contrario.)
Esto permite que la jerarquía de clases se extienda arbitrariamente, pudiendo las adiciones posteriores interpolar entre valores conocidos anteriormente y ellos mismos.
En algunos casos, la interpolación en sí no se puede describir por ninguna de
las clases disponibles, y se define una clase privada para describir la
etapa intermedia. Este es el caso, por ejemplo, cuando se interpola
entre un CircleBorder y un RoundedRectangleBorder.
Este mecanismo tiene una ventaja adicional: puede manejar la interpolación
desde etapas intermedias hacia nuevos valores. Por ejemplo, a mitad de camino
de una transición de círculo a cuadrado, la forma se podría cambiar una vez más,
haciendo que la animación necesite interpolar hacia un triángulo. Mientras
la clase del triángulo pueda hacer lerpFrom la clase intermedia del cuadrado redondeado,
la transición se puede realizar sin problemas.
Conclusión
#El lema de Flutter, "todo es un widget", gira en torno a construir interfaces de usuario componiendo widgets que, a su vez, están compuestos de widgets progresivamente más básicos. El resultado de esta composabilidad agresiva es una gran cantidad de widgets que requieren algoritmos y estructuras de datos cuidadosamente diseñados para procesarse eficientemente. Con algún diseño adicional, estas estructuras de datos también facilitan a los desarrolladores crear listas de desplazamiento infinito que construyen widgets bajo demanda a medida que se vuelven visibles.
Footnotes:
-
Para el layout, al menos. Se podría revisar para el pintado, para construir el árbol de accesibilidad si es necesario, y para hit testing si es necesario. ↩
-
La realidad, por supuesto, es un poco más complicada. Algunos layouts involucran dimensiones intrínsecas o mediciones de línea de base (baseline), las cuales involucran un recorrido adicional del subárbol relevante (se utiliza un almacenamiento en caché agresivo para mitigar el potencial de un rendimiento cuadrático en el peor de los casos). Estos casos, sin embargo, son sorprendentemente raros. En particular, las dimensiones intrínsecas no son requeridas para el caso común de shrink-wrapping. ↩
-
Técnicamente, la posición del hijo no es parte de su geometría RenderBox y, por lo tanto, no necesita calcularse realmente durante el layout. Muchos render objects posicionan implícitamente a su único hijo en 0,0 en relación con su propio origen, lo que no requiere cálculo ni almacenamiento en absoluto. Algunos render objects evitan calcular la posición de sus hijos hasta el último momento posible (por ejemplo, durante la fase de pintado), para evitar el cálculo por completo si no se pintan posteriormente. ↩
-
Existe una excepción a esta regla. Como se discutió en la sección Construcción de widgets bajo demanda, algunos widgets se pueden volver a construir como resultado de un cambio en las restricciones de layout. Si un widget se marcó como sucio (dirty) por razones no relacionadas en el mismo fotograma en el que también se ve afectado por un cambio en las restricciones de layout, se actualizará dos veces. Esta construcción redundante se limita al widget en sí y no impacta a sus descendientes. ↩
-
Una key es un objeto opaco asociado opcionalmente con un widget cuyo operador de igualdad se utiliza para influir en el algoritmo de reconciliación. ↩
-
Por accesibilidad, y para dar a las aplicaciones unos milisegundos extra entre el momento en que se construye un widget y el momento en que aparece en la pantalla, el viewport crea (pero no pinta) widgets para unos cientos de píxeles antes y después de los widgets visibles. ↩
-
Este enfoque fue popularizado por primera vez por la librería React de Facebook. ↩
-
En la práctica, se permite que el valor de t se extienda más allá del rango 0.0-1.0, y así lo hace para algunas curvas. Por ejemplo, las curvas "elásticas" se sobrepasan brevemente para representar un efecto de rebote. La lógica de interpolación típicamente puede extrapolar más allá del inicio o fin según sea apropiado. Para algunos tipos, por ejemplo, al interpolar colores, el valor de t se limita (clamped) efectivamente al rango 0.0-1.0. ↩
A menos que se indique lo contrario, la documentación de este sitio refleja Flutter 3.44.0. Página actualizada por última vez el 2026-05-05. Ver código fuente oreportar un problema.