Descripción general de la arquitectura de Flutter
Una descripción general de alto nivel de la arquitectura de Flutter, incluidos los principios y conceptos fundamentales que forman su diseño.
Este artículo está destinado a proporcionar una descripción general de alto nivel de la arquitectura de Flutter, incluidos los principios y conceptos fundamentales que forman su diseño. Si estás interesado en cómo estructurar la arquitectura de una aplicación Flutter, consulta Arquitectura de aplicaciones Flutter.
Flutter es un toolkit de UI multiplataforma diseñado para permitir la reutilización de código en sistemas operativos como iOS, Android, web y escritorio, al tiempo que permite que las aplicaciones se integren directamente con los servicios subyacentes de la plataforma. El objetivo es permitir a los desarrolladores entregar aplicaciones de alto rendimiento que se sientan naturales en diferentes plataformas, aprovechando las diferencias donde existan mientras se comparte tanto código como sea posible.
Durante el desarrollo, las aplicaciones de Flutter se ejecutan en una VM que ofrece Hot Reload stateful de los cambios sin necesidad de una recompilación completa. Para la versión final (release), las aplicaciones de Flutter se compilan directamente a código máquina, ya sean instrucciones Intel x64 o ARM, o a JavaScript si el objetivo es la web. El framework es de código abierto, con una licencia BSD permisiva, y cuenta con un próspero ecosistema de paquetes de terceros que complementan la funcionalidad de la biblioteca principal.
Esta descripción general está dividida en varias secciones:
- El modelo de capas: Las piezas a partir de las cuales se construye Flutter.
- Interfaces de usuario reactivas: Un concepto central para el desarrollo de interfaces de usuario en Flutter.
- Una introducción a los Widgets: Los bloques de construcción fundamentales de las interfaces de usuario de Flutter.
- El proceso de renderizado: Cómo Flutter transforma el código de UI en píxeles.
- Una descripción general de los embedders de plataforma: El código que permite a los SO móviles y de escritorio ejecutar aplicaciones Flutter.
- Integrando Flutter con otro código: Información sobre diferentes técnicas disponibles para aplicaciones Flutter.
- Soporte para la web: Observaciones finales sobre las características de Flutter en un entorno de navegador.
Capas arquitectónicas
#Flutter está diseñado como un sistema extensible por capas. Existe como una serie de bibliotecas independientes que dependen cada una de la capa subyacente. Ninguna capa tiene acceso privilegiado a la capa inferior, y cada parte del nivel del framework está diseñada para ser opcional y reemplazable.
Para el sistema operativo subyacente, las aplicaciones de Flutter se empaquetan de la misma manera que cualquier otra aplicación nativa. Un embedder específico de la plataforma proporciona un punto de entrada; se coordina con el sistema operativo subyacente para acceder a servicios como superficies de renderizado, accesibilidad y entrada; y gestiona el bucle de eventos de mensajes. El embedder está escrito en un lenguaje apropiado para la plataforma: actualmente Java y C++ para Android, Swift y Objective-C/Objective-C++ para iOS y macOS, y C++ para Windows y Linux. Utilizando el embedder, el código de Flutter se puede integrar en una aplicación existente como un módulo, o el código puede ser todo el contenido de la aplicación. Flutter incluye varios embedders para plataformas objetivo comunes, pero también existen otros embedders.
En el núcleo de Flutter está el engine de Flutter, que está escrito principalmente en C++ y admite las primitivas necesarias para respaldar todas las aplicaciones de Flutter. El engine es responsable de rasterizar escenas compuestas cada vez que se necesita pintar un nuevo cuadro (frame). Proporciona la implementación de bajo nivel de la API principal de Flutter, incluyendo diseño de texto gráfico, E/S de archivos y red, un runtime de Dart, y cadena de herramientas de compilación.
El engine se expone al framework de Flutter a través de
dart:ui,
que envuelve el código C++ subyacente en clases de Dart. Esta biblioteca
expone las primitivas de más bajo nivel, como clases para gestionar subsistemas de entrada,
gráficos y renderizado de texto.
Típicamente, los desarrolladores interactúan con Flutter a través del framework de Flutter, que proporciona un framework moderno y reactivo escrito en el lenguaje Dart. Incluye un rico conjunto de bibliotecas de plataforma, layout y fundamentales, compuestas por una serie de capas. Trabajando de abajo hacia arriba, tenemos:
- Clases fundamentales básicas, y servicios de bloques de construcción como animación, pintado y gestos que ofrecen abstracciones comúnmente utilizadas sobre la base subyacente.
- La capa de renderizado proporciona una abstracción para gestionar el layout. Con esta capa, puedes construir un árbol de objetos renderizables. Puedes manipular estos objetos dinámicamente, y el árbol actualizará automáticamente el layout para reflejar tus cambios.
- La capa de Widgets es una abstracción de composición. Cada RenderObject en la capa de renderizado tiene una clase correspondiente en la capa de Widgets. Además, la capa de Widgets te permite definir combinaciones de clases que puedes reutilizar. Esta es la capa en la que se introduce el modelo de programación reactiva.
- Las bibliotecas de Material y Cupertino ofrecen conjuntos exhaustivos de controles que utilizan las primitivas de composición de la capa de Widgets para implementar los lenguajes de diseño de Material o iOS.
El framework de Flutter es relativamente pequeño; muchas características de nivel superior que los desarrolladores podrían usar están implementadas como paquetes, incluyendo plugins de plataforma como camera y webview, así como características independientes de la plataforma como characters, http y animations que se construyen sobre las bibliotecas principales de Dart y Flutter. Algunos de estos paquetes provienen del ecosistema más amplio, cubriendo servicios como pagos en la aplicación, autenticación de Apple y animaciones.
El resto de esta descripción general navega a grandes rasgos hacia abajo por las capas, comenzando con el paradigma reactivo del desarrollo de UI. Luego, describimos cómo se componen los Widgets juntos y se convierten en objetos que se pueden renderizar como parte de una aplicación. Describimos cómo Flutter interoperará con otro código a nivel de plataforma, antes de dar un breve resumen de cómo difiere el soporte web de Flutter respecto a otros objetivos.
Anatomía de una aplicación
#El siguiente diagrama ofrece una vista general de las piezas
que componen una aplicación regular de Flutter generada por flutter create.
Muestra dónde se ubica el Engine de Flutter en este stack,
destaca los límites de las API e identifica los repositorios
donde residen las piezas individuales. La leyenda a continuación aclara
parte de la terminología comúnmente utilizada para describir las
piezas de una aplicación Flutter.
Dart App
- Compone Widgets en la interfaz de usuario deseada.
- Implementa la lógica de negocio.
- Propiedad del desarrollador de la aplicación.
Framework (código fuente)
- Proporciona una API de nivel superior para construir aplicaciones de alta calidad (por ejemplo, Widgets, hit-testing, detección de gestos, accesibilidad, entrada de texto).
- Compone el árbol de Widgets de la aplicación en una escena.
Engine (código fuente)
- Responsable de rasterizar las escenas compuestas.
- Proporciona la implementación de bajo nivel de las API principales de Flutter (por ejemplo, gráficos, layout de texto, runtime de Dart).
- Expone su funcionalidad al framework utilizando la API dart:ui.
- Se integra con una plataforma específica utilizando la API del Embedder del Engine.
Embedder (código fuente)
- Se coordina con el sistema operativo subyacente para acceder a servicios como superficies de renderizado, accesibilidad y entrada.
- Gestiona el bucle de eventos.
- Expone una API específica de la plataforma para integrar el Embedder en las aplicaciones.
Runner
- Compone las piezas expuestas por la API específica de la plataforma del Embedder en un paquete de aplicación ejecutable en la plataforma objetivo.
- Parte de la plantilla de aplicación generada por
flutter create, propiedad del desarrollador de la aplicación.
Interfaces de usuario reactivas
#En la superficie, Flutter es un framework de UI reactivo y declarativo, en el que el desarrollador proporciona un mapeo desde el State de la aplicación hacia el State de la interfaz, y el framework asume la tarea de actualizar la interfaz en tiempo de ejecución cuando el State de la aplicación cambia. Este modelo está inspirado en el trabajo proveniente de Facebook para su propio framework React, el cual incluye un replanteamiento de muchos principios de diseño tradicionales.
En la mayoría de los frameworks de UI tradicionales, el State inicial de la interfaz de usuario se describe una vez y luego el código del usuario lo actualiza por separado en tiempo de ejecución, en respuesta a eventos. Un desafío de este enfoque es que, a medida que la aplicación crece en complejidad, el desarrollador debe estar al tanto de cómo los cambios de State se propagan en cascada a lo largo de toda la UI. Por ejemplo, considera la siguiente UI:
Hay muchos lugares donde el State puede cambiar: la casilla de color, el deslizador de tono, los botones de opción. A medida que el usuario interactúa con la UI, los cambios deben reflejarse en todos los demás lugares. Peor aún, a menos que se tenga cuidado, un cambio menor en una parte de la interfaz de usuario puede causar efectos secundarios en partes de código aparentemente no relacionadas.
Una solución a esto es un enfoque como MVC, donde envías los cambios de datos al modelo a través del controlador, y luego el modelo envía el nuevo State a la vista a través del controlador. Sin embargo, esto también es problemático, ya que crear y actualizar elementos de la UI son dos pasos separados que pueden desincronizarse fácilmente.
Flutter, junto con otros frameworks reactivos, adopta un enfoque alternativo a este problema, desacoplando explícitamente la interfaz de usuario de su State subyacente. Con API de estilo React, solo creas la descripción de la UI, y el framework se encarga de usar esa única configuración para crear y/o actualizar la interfaz de usuario según corresponda.
En Flutter, los Widgets (semejantes a los componentes en React) están representados por clases inmutables que se utilizan para configurar un árbol de objetos. Estos Widgets se utilizan para gestionar un árbol independiente de objetos para el layout, que luego se utiliza para gestionar un árbol independiente de objetos para la composición. Flutter es, en su núcleo, una serie de mecanismos para recorrer eficientemente las partes modificadas de los árboles, convirtiendo árboles de objetos en árboles de objetos de nivel inferior y propagando cambios a través de estos árboles.
Un Widget declara su interfaz de usuario sobrescribiendo el método build(), que
es una función que convierte el State en UI:
UI = f(state)
El método build() está diseñado para ejecutarse rápidamente y debe estar libre de efectos
secundarios, lo que permite que el framework lo llame cada vez que sea necesario (potencialmente
con tan alta frecuencia como una vez por frame renderizado).
Este enfoque se basa en ciertas características del runtime de un lenguaje (en particular, la instanciación y eliminación rápida de objetos). Afortunadamente, Dart es particularmente adecuado para esta tarea.
Widgets
#Como se mencionó, Flutter enfatiza los Widgets como una unidad de composición. Los Widgets son los bloques de construcción de la interfaz de usuario de una aplicación Flutter, y cada Widget es una declaración inmutable de una parte de la interfaz de usuario.
Los Widgets forman una jerarquía basada en la composición. Cada Widget se anida dentro de su
padre y puede recibir contexto del padre. Esta estructura se extiende hasta llegar
al Widget raíz (el contenedor que aloja la aplicación Flutter, típicamente
MaterialApp o CupertinoApp), como muestra este ejemplo trivial:
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
void main() => runApp(const MyApp());
class MyApp extends StatelessWidget {
const MyApp({super.key});
@override
Widget build(BuildContext context) {
return MaterialApp(
home: Scaffold(
appBar: AppBar(title: const Text('My Home Page')),
body: Center(
child: Builder(
builder: (context) {
return Column(
children: [
const Text('Hello World'),
const SizedBox(height: 20),
ElevatedButton(
onPressed: () {
print('Click!');
},
child: const Text('A button'),
),
],
);
},
),
),
),
);
}
}
En el código anterior, todas las clases instanciadas son Widgets.
Las aplicaciones actualizan su interfaz de usuario en respuesta a eventos (como una interacción del usuario) indicándole al framework que reemplace un Widget en la jerarquía por otro Widget. El framework luego compara los Widgets nuevos y viejos, y actualiza eficientemente la interfaz de usuario.
Flutter tiene sus propias implementaciones de cada control de UI, en lugar de diferir a los proporcionados por el sistema: por ejemplo, hay una implementación en Dart pura tanto para el control Toggle de iOS como para la versión correspondiente del equivalente en Android.
Este enfoque proporciona varios beneficios:
- Permite una extensibilidad ilimitada. Un desarrollador que quiera una variante del control Switch puede crearla de cualquier forma arbitraria, y no está limitado a los puntos de extensión proporcionados por el SO.
- Evita un cuello de botella de rendimiento significativo al permitir que Flutter componga toda la escena a la vez, sin cambiar de un lado a otro entre el código de Flutter y el código de la plataforma.
- Desacopla el comportamiento de la aplicación de cualquier dependencia del sistema operativo. La aplicación se ve y se siente igual en todas las versiones del SO, incluso si el SO cambió las implementaciones de sus controles.
Composición
#Los Widgets se componen típicamente de muchos otros Widgets pequeños y de propósito único que se combinan para producir efectos potentes.
Donde es posible, el número de conceptos de diseño se mantiene al mínimo mientras se
permite que el vocabulario total sea amplio. Por ejemplo, en la capa de Widgets,
Flutter utiliza el mismo concepto central (un Widget) para representar el dibujo en la
pantalla, el layout (posicionamiento y dimensionamiento), la interactividad del usuario, State Management,
los temas, las animaciones y la navegación. En la capa de animación, un par de conceptos,
Animations y Tweens, cubren la mayor parte del espacio de diseño. En la capa de
renderizado, los RenderObjects se utilizan para describir el layout, el pintado, el hit testing y la
accesibilidad. En cada uno de estos casos, el vocabulario correspondiente termina
siendo amplio: hay cientos de Widgets y RenderObjects, y docenas de
tipos de animaciones y Tweens.
La jerarquía de clases es deliberadamente poco profunda y amplia para maximizar el número posible
de combinaciones, enfocándose en Widgets pequeños y componibles que cada uno hace una
cosa bien. Las características principales son abstractas, e incluso características básicas como el padding
y la alineación están implementadas como componentes separados en lugar de estar integradas
en el núcleo. (Esto también contrasta con las API más tradicionales donde características
como el padding están integradas en el núcleo común de cada componente de layout). Por lo tanto, por
ejemplo, para centrar un Widget, en lugar de ajustar una propiedad nocional de Align,
lo envuelves en un Widget Center.
Hay Widgets para padding, alineación, filas, columnas y cuadrículas. Estos Widgets de layout no tienen una representación visual propia. En su lugar, su único propósito es controlar algún aspecto del layout de otro Widget. Flutter también incluye Widgets de utilidad que aprovechan este enfoque de composición.
Por ejemplo, Container, un
Widget comúnmente utilizado, está compuesto por varios Widgets responsables del layout,
pintado, posicionamiento y dimensionamiento. Específicamente, Container está compuesto por los
Widgets LimitedBox,
ConstrainedBox,
Align,
Padding,
DecoratedBox y
Transform, como
puedes ver al leer su código fuente. Una característica definitoria de Flutter es que
puedes profundizar en el código fuente de cualquier Widget y me examinarlo. Por lo tanto, en lugar
de crear una subclase de Container para producir un efecto personalizado, puedes componerlo
a él y a otros Widgets de formas novedosas, o simplemente crear un nuevo Widget utilizando
Container como inspiración.
Construyendo Widgets
#Como se mencionó anteriormente, determinas la representación visual de un Widget sobrescribiendo la
función build() para
retornar un nuevo árbol de elementos. Este árbol representa la parte de la interfaz
de usuario correspondiente al Widget en términos más concretos. Por ejemplo, un Widget de barra de herramientas puede tener una
función build que retorne un layout horizontal de algo de
texto y
varios
botones. Según sea necesario,
el framework le pide de forma recursiva a cada Widget que se construya hasta que el árbol esté completamente
descrito por objetos renderizables concretos. El
framework luego une los objetos renderizables en un árbol de objetos renderizables.
La función build de un Widget debe estar libre de efectos secundarios. Cada vez que se solicita a la función que construya, el Widget debe retornar un nuevo árbol de Widgets1, independientemente de lo que el Widget haya retornado previamente. El framework hace el trabajo pesado para determinar qué métodos build necesitan ser llamados basándose en el árbol de objetos de renderizado (descrito con más detalle más adelante). Se puede encontrar más información sobre este proceso en el tema Inside Flutter.
En cada frame renderizado, Flutter puede recrear solo las partes de la UI donde el
State ha cambiado llamando al método build() de ese Widget. Por lo tanto, es
importante que los métodos build se ejecuten rápidamente, y el trabajo computacional pesado
debe realizarse de manera asíncrona y luego almacenarse como parte del State
para que lo utilice un método build.
Aunque es un enfoque relativamente ingenuo, esta comparación automatizada es bastante efectiva, permitiendo aplicaciones interactivas de alto rendimiento. Además, el diseño de la función build simplifica tu código al enfocarse en declarar de qué está compuesto un Widget, en lugar de las complejidades de actualizar la interfaz de usuario de un State a otro.
State de los Widgets
#El framework introduce dos clases principales de Widgets: Widgets Stateful y Stateless.
Muchos Widgets no tienen un State mutable: no tienen ninguna propiedad que cambie
con el tiempo (por ejemplo, un icono o una etiqueta). Estos Widgets heredan de
StatelessWidget.
Sin embargo, si las características únicas de un Widget necesitan cambiar basándose en la
interacción del usuario u otros factores, ese Widget es Stateful. Por ejemplo, si un
Widget tiene un contador que se incrementa cada vez que el usuario toca un botón, entonces el
valor del contador es el State para ese Widget. Cuando ese valor cambia, el
Widget necesita ser reconstruido para actualizar su parte de la UI. Estos Widgets heredan de
StatefulWidget y,
(debido a que el Widget en sí es inmutable) almacenan el State mutable en una clase separada
que hereda de State.
Los StatefulWidgets no tienen un método build; en su lugar, su interfaz de usuario se
construye a través de su objeto State.
Cada vez que mutas un objeto State (por ejemplo, al incrementar el contador),
debes llamar a setState()
para indicarle al framework que actualice la interfaz de usuario llamando al método
build del State nuevamente.
Tener objetos de State y Widget separados permite que otros Widgets traten tanto a los Widgets Stateless como a los Stateful exactamente de la misma manera, sin preocuparse por perder el State. En lugar de necesitar mantener un hijo para preservar su State, el padre puede crear una nueva instancia del hijo en cualquier momento sin perder el State persistente del hijo. El framework hace todo el trabajo de encontrar y reutilizar objetos de State existentes cuando es apropiado.
State Management
#Por lo tanto, si muchos Widgets pueden contener State, ¿cómo se gestiona y distribuye el State por el sistema?
Al igual que con cualquier otra clase,
puedes usar un constructor en un Widget para inicializar sus datos,
de modo que un método build() pueda asegurar que cualquier Widget hijo
se instancie con los datos que necesita:
@override
Widget build(BuildContext context) {
return ContentWidget(importantState);
}
Donde importantState es un marcador de posición para la clase
que contiene el State importante para el Widget.
Sin embargo, a medida que los árboles de Widgets se vuelven más profundos,
pasar información de State hacia arriba y hacia abajo en la
jerarquía del árbol se vuelve engorroso.
Por lo tanto, un tercer tipo de Widget, InheritedWidget,
proporciona una forma fácil de obtener datos de un ancestro compartido.
Puedes usar InheritedWidget para crear un Widget de State
que envuelva a un ancestro común en el
árbol de Widgets, como se muestra en este ejemplo:
Cada vez que uno de los objetos ExamWidget o GradeWidget necesita datos de
StudentState, ahora puede acceder a ellos con un comando como:
final studentState = StudentState.of(context);
La llamada of(context) toma el BuildContext
(una referencia a la ubicación actual del Widget),
y devuelve el ancestro más cercano en el árbol
que coincide con el tipo StudentState.
Los InheritedWidgets también ofrecen un método updateShouldNotify(),
al cual Flutter llama para determinar si un cambio de State
debería activar una reconstrucción de los Widgets hijos que lo utilizan.
El propio Flutter utiliza InheritedWidget extensamente como parte
del framework para el State compartido,
como el tema visual de la aplicación, que incluye
propiedades como el color y los estilos tipográficos
que son
generalizados a lo largo de una aplicación.
El método build() de MaterialApp inserta un tema
en el árbol cuando se construye, y luego, más profundamente en la jerarquía, un Widget
puede usar el método .of() para buscar los datos del tema relevantes.
Por ejemplo:
Container(
color: Theme.of(context).secondaryHeaderColor,
child: Text(
'Text with a background color',
style: Theme.of(context).textTheme.titleLarge,
),
);
A medida que las aplicaciones crecen, los enfoques más avanzados de State Management que reducen el
trabajo repetitivo de crear y usar Widgets Stateful se vuelven más atractivos. Muchas
aplicaciones Flutter utilizan paquetes de utilidad como
Provider, que proporciona una envoltura alrededor de
InheritedWidget. La arquitectura por capas de Flutter también permite enfoques alternativos
para implementar la transformación de State a UI, como el paquete
flutter_hooks.
Renderizado y layout
#Esta sección describe el pipeline de renderizado, que es la serie de pasos que Flutter sigue para convertir una jerarquía de Widgets en los píxeles reales pintados en una pantalla.
El modelo de renderizado de Flutter
#Te estarás preguntando: si Flutter es un framework multiplataforma, ¿cómo puede ofrecer un rendimiento comparable al de los frameworks de una sola plataforma?
Es útil comenzar pensando en cómo funcionan las aplicaciones tradicionales
de Android. Al dibujar,
primero llamas al código Java del framework de Android.
Las bibliotecas del sistema de Android proporcionan los componentes
responsables de dibujarse a sí mismos en un objeto Canvas,
que Android puede luego renderizar con Skia,
un motor de gráficos escrito en C/C++ que llama a la
CPU o GPU para completar el dibujo en el dispositivo.
Los frameworks multiplataforma funcionan típicamente creando una capa de abstracción sobre las bibliotecas nativas de UI subyacentes de Android e iOS, intentando suavizar las inconsistencias de la representación de cada plataforma. El código de la aplicación a menudo se escribe en un lenguaje interpretado como JavaScript, el cual debe a su vez interactuar con las bibliotecas del sistema de Android basadas en Java o de iOS basadas en Objective-C para mostrar la UI. Todo esto agrega una sobrecarga que puede ser significativa, particularmente donde hay mucha interacción entre la UI y la lógica de la aplicación.
Por el contrario, Flutter minimiza esas abstracciones, omitiendo las bibliotecas de Widgets de la UI del sistema a favor de su propio conjunto de Widgets. El código Dart que pinta los elementos visuales de Flutter se compila a código nativo, el cual utiliza Impeller para el renderizado. Impeller se incluye junto con la aplicación, lo que permite al desarrollador actualizar su aplicación para mantenerse al día con las últimas mejoras de rendimiento incluso si el teléfono no se ha actualizado a una nueva versión de Android. Lo mismo ocurre con Flutter en otras plataformas nativas, como Windows o macOS.
Desde la entrada del usuario hasta la GPU
#El principio primordial que Flutter aplica a su pipeline de renderizado es que lo simple es rápido. Flutter tiene un pipeline sencillo para cómo fluyen los datos hacia el sistema, como se muestra en el siguiente diagrama de secuencia:
Echemos un vistazo a algunas de estas fases con mayor detalle.
Build: de Widget a Element
#Considera este fragmento de código que demuestra una jerarquía de Widgets:
Container(
color: Colors.blue,
child: Row(
children: [
Image.network('https://www.example.com/1.png'),
const Text('A'),
],
),
);
Cuando Flutter necesita renderizar este fragmento,
llama al método build(), el cual
retorna un subárbol de Widgets que renderiza
la UI basándose en el State actual de la aplicación.
Durante este proceso,
el método build() puede introducir nuevos Widgets,
según sea necesario, basándose en su State.
Como ejemplo, en el fragmento de código anterior,
Container tiene propiedades color y child.
Al mirar el código
fuente
de Container, puedes ver que si el color no es nulo,
inserta un ColoredBox que representa el color:
if (color != null)
current = ColoredBox(color: color!, child: current);
Correspondientemente, los Widgets Image y Text podrían insertar Widgets hijos tales
como RawImage y RichText durante el proceso de construcción. La jerarquía
eventual de Widgets podría ser, por lo tanto, más profunda de lo que el código representa,
como en este caso2:
Esto explica por qué, cuando examinas el árbol a través de una herramienta de depuración como el inspector de Flutter, parte de Flutter/Dart DevTools, puedes ver una estructura que es considerablemente más profunda de lo que está en tu código original.
Durante la fase de construcción (build), Flutter traduce los Widgets expresados en código a un árbol de elementos correspondiente, con un elemento por cada Widget. Cada elemento representa una instancia específica de un Widget en una ubicación dada de la jerarquía del árbol. Hay dos tipos básicos de elementos:
ComponentElement, un host para otros elementos.RenderObjectElement, un elemento que participa en las fases de layout o pintado.
Los RenderObjectElements son un intermediario entre su análogo Widget y el
RenderObject subyacente, al cual llegaremos más adelante.
El elemento de cualquier Widget se puede referenciar a través de su BuildContext, el cual
es una referencia a la ubicación de un Widget en el árbol. Este es el context
en una
llamada a función como Theme.of(context), y se suministra al método build()
como parámetro.
Debido a que los Widgets son inmutables, incluida la relación padre/hijo entre
nodos, cualquier cambio en el árbol de Widgets (como cambiar Text('A') a
Text('B') en el ejemplo anterior) hace que se devuelva un nuevo conjunto de objetos Widget.
Pero eso no significa que la representación subyacente deba ser reconstruida.
El árbol de elementos es persistente de frame a frame y, por lo tanto, desempeña un
papel crítico de rendimiento, permitiendo que Flutter actúe como si la jerarquía de Widgets fuera
completamente desechable mientras almacena en caché su representación subyacente. Al recorrer únicamente
los Widgets que cambiaron, Flutter puede reconstruir solo las partes del
árbol de elementos que requieren reconfiguración.
Layout y renderizado
#Sería una aplicación rara la que dibujara solo un Widget. Una parte importante de cualquier framework de UI es, por lo tanto, la capacidad de posicionar (layout) eficientemente una jerarquía de Widgets, determinando el tamaño y la posición de cada elemento antes de que sean renderizados en la pantalla.
La clase base para cada nodo en el árbol de renderizado es
RenderObject, la cual
define un modelo abstracto para layout y pintado. Esto es extremadamente general: no
se compromete con un número fijo de dimensiones o incluso con un sistema de coordenadas cartesiano
(demostrado por este ejemplo de un sistema de coordenadas
polares). Cada
RenderObject conoce a su padre, pero sabe poco sobre sus hijos más allá de
cómo visitarlos y sus restricciones. Esto le proporciona a RenderObject la
suficiente abstracción para ser capaz de manejar una variedad de casos de uso.
Durante la fase de construcción (build), Flutter crea o actualiza un objeto que hereda de
RenderObject para cada RenderObjectElement en el árbol de elementos.
Los RenderObjects son primitivas:
RenderParagraph
renderiza texto,
RenderImage
renderiza
una imagen, y
RenderTransform
aplica una transformación antes de pintar a su hijo.
La mayoría de los Widgets de Flutter son renderizados por un objeto que hereda de la
subclase RenderBox, la cual representa un RenderObject de tamaño fijo en un espacio
cartesiano 2D. RenderBox proporciona la base de un modelo de restricciones de caja (box constraints),
estableciendo un ancho y alto mínimo y máximo para cada Widget a
renderizar.
Para realizar el layout, Flutter recorre el árbol de renderizado en un recorrido en profundidad (depth-first traversal) y transfiere las restricciones de tamaño de padre a hijo. Al determinar su tamaño, el hijo debe respetar las restricciones impuestas por su padre. Los hijos responden devolviendo un tamaño a su objeto padre dentro de las restricciones que el padre estableció.
Al final de este único recorrido por el árbol, cada objeto tiene un tamaño definido
dentro de las restricciones de su padre y está listo para ser pintado llamando al
método paint().
El modelo de restricciones de caja es muy potente como forma de posicionar objetos en tiempo O(n):
- Los padres pueden dictar el tamaño de un objeto hijo estableciendo las restricciones máximas y mínimas al mismo valor. Por ejemplo, el RenderObject superior en una aplicación de teléfono restringe a su hijo a ser del tamaño de la pantalla. (Los hijos pueden elegir cómo usar ese espacio. Por ejemplo, simplemente podrían centrar lo que quieren renderizar dentro de las restricciones dictadas).
- Un padre puede dictar el ancho del hijo pero darle al hijo flexibilidad sobre la altura (o dictar la altura pero ofrecer flexibilidad sobre el ancho). Un ejemplo del mundo real es el texto fluido, que podría tener que ajustarse a una restricción horizontal pero variar verticalmente según la cantidad de texto.
Este modelo funciona incluso cuando un objeto hijo necesita saber cuánto espacio tiene
disponible para decidir cómo renderizar su contenido. Al usar un Widget
LayoutBuilder,
el objeto hijo puede examinar las restricciones pasadas hacia abajo y usarlas para
determinar cómo las utilizará, por ejemplo:
Widget build(BuildContext context) {
return LayoutBuilder(
builder: (context, constraints) {
if (constraints.maxWidth < 600) {
return const OneColumnLayout();
} else {
return const TwoColumnLayout();
}
},
);
}
Más información sobre las restricciones y el sistema de layout, junto con ejemplos prácticos, se pueden encontrar en el tema Entendiendo las restricciones.
La raíz de todos los RenderObjects es el RenderView, el cual representa la salida
total del árbol de renderizado. Cuando la plataforma exige renderizar un nuevo frame
(por ejemplo, debido a una señal de
vsync o porque
se completó la descompresión/carga de una textura), se realiza una llamada al método
compositeFrame(), que es parte del objeto RenderView en la raíz
del árbol de renderizado. Esto crea un SceneBuilder para activar una actualización de la
escena. Cuando la escena está completa, el objeto RenderView pasa la escena
compuesta al método Window.render() en dart:ui, el cual pasa el control a la
GPU para renderizarla.
Más detalles de las etapas de composición y rasterización del pipeline van más allá del alcance de este artículo de alto nivel, pero se puede encontrar más información en esta charla sobre el pipeline de renderizado de Flutter.
Embebido en plataformas
#Como hemos visto, en lugar de traducirse en los Widgets equivalentes del sistema operativo, las interfaces de usuario de Flutter son construidas, posicionadas (layout), compuestas y pintadas por la propia Flutter. El mecanismo para obtener la textura y participar en el ciclo de vida de la aplicación del sistema operativo subyacente varía inevitablemente según las preocupaciones únicas de esa plataforma. El engine es independiente de la plataforma, presentando una ABI (Interfaz Binaria de Aplicación) estable que proporciona a un embedder de plataforma una forma de configurar y utilizar Flutter.
El embedder de plataforma es la aplicación nativa del sistema operativo que aloja todo el contenido de Flutter, y actúa como el pegamento entre el sistema operativo anfitrión y Flutter. Cuando inicias una aplicación Flutter, el embedder proporciona el punto de entrada, inicializa el engine de Flutter, obtiene subprocesos (threads) para la interfaz de usuario y la rasterización, y crea una textura en la que Flutter puede escribir. El embedder también es responsable del ciclo de vida de la aplicación, incluidos los gestos de entrada (como ratón, teclado, táctil), el tamaño de la ventana, la gestión de subprocesos y los mensajes de plataforma. Flutter incluye embedders de plataforma para Android, iOS, Windows, macOS y Linux; también puedes crear un embedder de plataforma personalizado, como en este ejemplo práctico que admite sesiones remotas de Flutter a través de un framebuffer de estilo VNC o este ejemplo práctico para Raspberry Pi.
Cada plataforma tiene su propio conjunto de API y restricciones. Algunas breves notas específicas de cada plataforma:
- A partir de Flutter 3.29, los subprocesos de la interfaz de usuario y de la plataforma están fusionados en iOS y Android. Específicamente, el subproceso de UI se elimina y el código Dart se ejecuta en el subproceso nativo de la plataforma. Para obtener más información, consulta el video The great thread merge.
- En iOS y macOS, Flutter se carga en el embedder como un
UIViewControlleroNSViewController, respectivamente. El embedder de plataforma crea unFlutterEngine, que sirve como anfitrión para la VM de Dart y tu runtime de Flutter, y unFlutterViewController, que se adjunta alFlutterEnginepara pasar eventos de entrada de UIKit o Cocoa hacia Flutter y para mostrar cuadros (frames) renderizados por elFlutterEngineusando Metal o OpenGL. - En Android, Flutter se carga, por defecto, en el embedder como una
Activity. La vista está controlada por unaFlutterView, la cual renderiza el contenido de Flutter ya sea como una vista o como una textura, según los requisitos de composición y orden Z (z-ordering) del contenido de Flutter. - En Windows, Flutter se aloja en una aplicación Win32 tradicional, y el contenido se renderiza usando ANGLE, una biblioteca que traduce llamadas a la API de OpenGL a sus equivalentes en DirectX 11.
Integración con otro código
#Flutter proporciona una variedad de mecanismos de interoperabilidad, ya sea que estés accediendo a código o API escritas en un lenguaje como Kotlin o Swift, llamando a una API nativa basada en C, embebiendo controles nativos en una aplicación Flutter o embebiendo Flutter en una aplicación existente.
platform channels (canales de plataforma)
#Para aplicaciones móviles y de escritorio, Flutter te permite realizar llamadas a código personalizado a través de
un Platform channel, que es un mecanismo de comunicación entre tu
código Dart y el código específico de plataforma de tu aplicación anfitriona. Al crear un
canal común (que encapsula un nombre y un codec), puedes enviar y recibir mensajes
entre Dart y un componente de la plataforma escrito en un lenguaje como Kotlin o
Swift. Los datos se serializan desde un tipo de Dart como Map a un formato estándar,
y luego se deserializan a una representación equivalente en Kotlin (como
HashMap) o Swift (como Dictionary).
A continuación se muestra un breve ejemplo de un Platform channel de una llamada de Dart a un gestor de eventos receptor en Kotlin (Android) o Swift (iOS):
// Dart side
const channel = MethodChannel('foo');
final greeting = await channel.invokeMethod('bar', 'world') as String;
print(greeting);
// Android (Kotlin)
val channel = MethodChannel(flutterView, "foo")
channel.setMethodCallHandler { call, result ->
when (call.method) {
"bar" -> result.success("Hello, ${call.arguments}")
else -> result.notImplemented()
}
}
// iOS (Swift)
let channel = FlutterMethodChannel(name: "foo", binaryMessenger: flutterView)
channel.setMethodCallHandler {
(call: FlutterMethodCall, result: FlutterResult) -> Void in
switch (call.method) {
case "bar": result("Hello, \(call.arguments as! String)")
default: result(FlutterMethodNotImplemented)
}
}
Se pueden encontrar más ejemplos del uso de Platform channels, incluyendo ejemplos para plataformas de escritorio, en el repositorio flutter/packages. También hay miles de plugins ya disponibles para Flutter que cubren muchos escenarios comunes, que van desde Firebase hasta anuncios y hardware del dispositivo como la cámara y Bluetooth.
Foreign Function Interface (FFI)
#Para las API basadas en C, incluidas las que se pueden generar para código escrito en
lenguajes modernos como Rust o Go, Dart proporciona un mecanismo directo para vincularse
a código nativo utilizando la biblioteca dart:ffi. El modelo de interfaz de funciones foráneas (FFI)
puede ser considerablemente más rápido que los Platform channels, porque no se requiere
serialización para pasar datos. En su lugar, el runtime de Dart proporciona la
capacidad de asignar memoria en el heap respaldada por un objeto Dart y realizar
llamadas a bibliotecas enlazadas estática o dinámicamente. FFI está disponible para todas las
plataformas excepto web, donde las bibliotecas de interoperabilidad JS
y
package:web cumplen un propósito similar.
Para usar FFI, creas un typedef para cada una de las firmas de métodos en Dart y no administrados,
y le indicas a la VM de Dart cómo mapear entre ellos. Como ejemplo,
aquí hay un fragmento de código para llamar a la API tradicional de Win32 MessageBox():
import 'dart:ffi';
import 'package:ffi/ffi.dart'; // contains .toNativeUtf16() extension method
typedef MessageBoxNative =
Int32 Function(
IntPtr hWnd,
Pointer<Utf16> lpText,
Pointer<Utf16> lpCaption,
Int32 uType,
);
typedef MessageBoxDart =
int Function(
int hWnd,
Pointer<Utf16> lpText,
Pointer<Utf16> lpCaption,
int uType,
);
void exampleFfi() {
final user32 = DynamicLibrary.open('user32.dll');
final messageBox = user32.lookupFunction<MessageBoxNative, MessageBoxDart>(
'MessageBoxW',
);
final result = messageBox(
0, // No owner window
'Test message'.toNativeUtf16(), // Message
'Window caption'.toNativeUtf16(), // Window title
0, // OK button only
);
}
Renderizado de controles nativos en una aplicación Flutter
#Debido a que el contenido de Flutter se dibuja en una textura y su árbol de Widgets es completamente interno, no hay espacio para que algo como una vista de Android exista dentro del modelo interno de Flutter o se renderice intercalado dentro de los Widgets de Flutter. Esto es un problema para los desarrolladores que deseen incluir componentes de plataforma existentes en sus aplicaciones Flutter, como un control de navegador.
Flutter resuelve esto introduciendo Widgets de vistas de plataforma (platform view widgets)
(AndroidView
y UiKitView)
que te permiten embeber este tipo de contenido en cada plataforma. Las vistas de plataforma pueden ser
integradas con otro contenido de Flutter3. Cada uno de
estos Widgets actúa como un intermediario con el sistema operativo subyacente. Por
ejemplo, en Android, AndroidView cumple tres funciones principales:
- Hacer una copia de la textura gráfica renderizada por la vista nativa y presentarla a Flutter para su composición como parte de una superficie renderizada por Flutter cada vez que se pinta el frame.
- Responder al hit testing y gestos de entrada, y traducirlos en la entrada nativa equivalente.
- Crear un análogo del árbol de accesibilidad y pasar comandos y respuestas entre las capas nativas y de Flutter.
Inevitablemente, hay una cierta cantidad de sobrecarga asociada con esta sincronización. En general, por lo tanto, este enfoque es más adecuado para controles complejos como Google Maps, donde la reimplementación en Flutter no es práctica.
Típicamente, una aplicación Flutter instanciará estos Widgets en un método build() basándose
en una prueba de plataforma. Como ejemplo, tomado del
plugin google_maps_flutter:
if (defaultTargetPlatform == TargetPlatform.android) {
return AndroidView(
viewType: 'plugins.flutter.io/google_maps',
onPlatformViewCreated: onPlatformViewCreated,
gestureRecognizers: gestureRecognizers,
creationParams: creationParams,
creationParamsCodec: const StandardMessageCodec(),
);
} else if (defaultTargetPlatform == TargetPlatform.iOS) {
return UiKitView(
viewType: 'plugins.flutter.io/google_maps',
onPlatformViewCreated: onPlatformViewCreated,
gestureRecognizers: gestureRecognizers,
creationParams: creationParams,
creationParamsCodec: const StandardMessageCodec(),
);
}
return Text(
'$defaultTargetPlatform is not yet supported by the maps plugin',
);
La comunicación con el código nativo subyacente al AndroidView o UiKitView
típicamente ocurre utilizando el mecanismo de Platform channels, como se describió previamente.
En la actualidad, las vistas de plataforma no están disponibles para plataformas de escritorio, pero esto no es una limitación arquitectónica; el soporte podría añadirse en el futuro.
Alojamiento de contenido Flutter en una aplicación principal
#El caso inverso del escenario anterior es embeber un Widget de Flutter en una
aplicación existente de Android o iOS. Como se describió en una sección anterior, una aplicación Flutter
recién creada ejecutándose en un dispositivo móvil se aloja en una Activity de Android o en un
UIViewController de iOS. El contenido de Flutter se puede embeber en una aplicación existente de Android o
iOS utilizando la misma API de embebido.
La plantilla de módulo de Flutter está diseñada para un embebido fácil; puedes embeberlo como una dependencia de código fuente en una definición de compilación existente de Gradle o Xcode, o puedes compilarlo en un binario de Android Archive o iOS Framework para su uso sin requerir que todos los desarrolladores tengan Flutter instalado.
El engine de Flutter tarda un breve momento en inicializarse, porque necesita cargar bibliotecas compartidas de Flutter, inicializar el runtime de Dart, crear y ejecutar un isolate de Dart y adjuntar una superficie de renderizado a la UI. Para minimizar cualquier retraso en la UI al presentar contenido de Flutter, es mejor inicializar el engine de Flutter durante la secuencia general de inicialización de la aplicación, o al menos antes de la primera pantalla de Flutter, para que los usuarios no experimenten una pausa repentina mientras el primer código de Flutter se carga. Además, separar el engine de Flutter permite que se reutilice en múltiples pantallas de Flutter y comparta la sobrecarga de memoria involucrada con la carga de las bibliotecas necesarias.
Más información sobre cómo se carga Flutter en una aplicación existente de Android o iOS se puede encontrar en el tema Secuencia de carga, rendimiento y memoria.
Soporte de Flutter para la web
#Si bien los conceptos arquitectónicos generales se aplican a todas las plataformas que Flutter admite, hay algunas características únicas del soporte web de Flutter que son dignas de comentar.
Dart ha estado compilando a JavaScript durante todo el tiempo que el lenguaje ha existido, con una cadena de herramientas optimizada tanto para propósitos de desarrollo como de producción. Muchas aplicaciones importantes compilan desde Dart a JavaScript y se ejecutan en producción hoy en día, incluyendo las herramientas para anunciantes de Google Ads. Debido a que el framework de Flutter está escrito en Dart, compilarlo a JavaScript fue relativamente sencillo.
Sin embargo, el engine de Flutter, escrito en C++, está diseñado para interactuar con el sistema operativo subyacente en lugar de con un navegador web. Por lo tanto, se requiere un enfoque diferente.
En la web, Flutter ofrece dos renderizadores:
| Renderizador | Objetivo de compilación |
|---|---|
| CanvasKit | JavaScript |
| Skwasm | WebAssembly |
Los modos de compilación (build modes) son opciones de la línea de comandos que dictan qué renderizadores están disponibles cuando ejecutas la aplicación.
Flutter ofrece dos modos de compilación (build):
| Modo de compilación | Renderizador(es) disponible(s) |
|---|---|
| por defecto | CanvasKit |
| `--wasm` | Skwasm (preferido), CanvasKit (fallback) |
El modo por defecto hace que solo esté disponible el renderizador CanvasKit.
La opción --wasm hace que ambos renderizadores estén disponibles,
y elige el engine basándose en las capacidades del navegador:
prefiriendo Skwasm si el navegador es capaz de ejecutarlo,
y usando CanvasKit como alternativa (fallback) de lo contrario.
Tal vez la diferencia más notable en comparación con otras plataformas en las que se ejecuta Flutter es que no hay necesidad de que Flutter proporcione un runtime de Dart. En su lugar, el framework de Flutter (junto con cualquier código que escribas) se compila a JavaScript. También vale la pena señalar que Dart tiene muy pocas diferencias semánticas de lenguaje en todos sus modos (JIT versus AOT, compilación nativa versus web), y la mayoría de los desarrolladores nunca escribirán una línea de código que se tope con tal diferencia.
Durante el tiempo de desarrollo, Flutter web utiliza
dartdevc,
un compilador que admite la compilación incremental
y, por lo tanto, permite Hot Restart y
[Hot Reload bajo una marca (flag)][].
Por el contrario, cuando estás listo para crear una aplicación de producción
para la web, se utiliza dart2js,
el compilador de JavaScript de producción altamente optimizado de Dart,
empaquetando el núcleo y el framework de Flutter junto con tu
aplicación en un archivo fuente minificado que
se puede desplegar en cualquier servidor web.
El código se puede ofrecer en un solo archivo o dividir
en múltiples archivos a través de importaciones diferidas (deferred imports).
Para obtener más información sobre Flutter web, consulta Soporte web para Flutter y Renderizadores web.
Más información
#Para aquellos interesados en más información sobre el funcionamiento interno de Flutter, el libro blanco Inside Flutter proporciona una guía útil sobre la filosofía de diseño del framework.
-
Si bien la función
buildretorna un árbol nuevo, solo necesitas retornar algo diferente si hay alguna nueva configuración para incorporar. Si la configuración es de hecho la misma, simplemente puedes retornar el mismo Widget. ↩ -
Esta es una ligera simplificación para facilitar la lectura. En la práctica, el árbol podría ser más complejo. ↩
-
Existen algunas limitaciones con este enfoque, por ejemplo, la transparencia no se compone de la misma manera para una vista de plataforma como lo haría para otros Widgets de Flutter. ↩
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.