Entendiendo las restricciones
El modelo de Flutter para restricciones de Widget, dimensionamiento, posicionamiento y cómo interactúan.
Cuando alguien que está aprendiendo Flutter te pregunta por qué algún Widget
con width: 100 no tiene 100 píxeles de ancho,
la respuesta por defecto es decirle que ponga ese Widget
dentro de un Center, ¿verdad?
Don't do that.
Si lo haces, volverán una y otra vez,
preguntando por qué algún FittedBox no funciona,
por qué ese Column se desborda, o qué
se supone que debe hacer IntrinsicWidth.
En su lugar, primero diles que el layout de Flutter es muy diferente del layout de HTML (que es de donde probablemente provienen), y luego haz que memoricen la siguiente regla:
El layout de Flutter no se puede entender realmente sin conocer esta regla, por lo que los desarrolladores de Flutter deberían aprenderla desde el principio.
En más detalle:
- Un Widget obtiene sus propias restricciones de su padre. Una restricción es solo un conjunto de 4 doubles: un ancho mínimo y máximo, y una altura mínima y máxima.
- Luego, el Widget recorre su propia lista de hijos. Uno por uno, el Widget les dice a sus hijos cuáles son sus restricciones (que pueden ser diferentes para cada hijo), y luego le pregunta a cada hijo qué tamaño quiere tener.
- Luego, el Widget posiciona a sus hijos
(horizontalmente en el eje
x, y verticalmente en el ejey), uno por uno. - Y, finalmente, el Widget le informa a su padre sobre su propio tamaño (dentro de las restricciones originales, por supuesto).
Por ejemplo, si un Widget compuesto contiene una columna con algo de padding, y quiere organizar a sus dos hijos de la siguiente manera:
La negociación es algo como esto:
Widget: "Oye padre, ¿cuáles son mis restricciones?"
Padre: "Debes tener de 0 a 300 píxeles de ancho,
y de 0 a 85 de alto."
Widget: "Hmmm, como quiero tener 5 píxeles de padding,
entonces mis hijos pueden tener como máximo 290 píxeles de ancho
y 75 píxeles de alto."
Widget: "Oye primer hijo, debes tener de 0 a 290
píxeles de ancho, y de 0 a 75 de alto."
Primer hijo: "Vale, entonces quiero tener 290 píxeles de ancho
y 20 píxeles de alto."
Widget: "Hmmm, como quiero colocar a mi segundo hijo debajo del
primero, esto deja solo 55 píxeles de altura para
mi segundo hijo."
Widget: "Oye segundo hijo, debes tener de 0 a 290 de ancho,
y de 0 a 55 de alto."
Segundo hijo: "Vale, quiero tener 140 píxeles de ancho,
y 30 píxeles de alto."
Widget: "Muy bien. Mi primer hijo tiene la posición x: 5 e y: 5,
y mi segundo hijo tiene x: 80 e y: 25."
Widget: "Oye padre, he decidido que mi tamaño va a ser de 300
píxeles de ancho, y 60 píxeles de alto."
Limitaciones
#El motor de layout de Flutter está diseñado para ser un proceso de una sola pasada. Esto significa que Flutter organiza sus Widgets de manera muy eficiente, pero da como resultado algunas limitaciones:
-
Un Widget puede decidir su propio tamaño solo dentro de las restricciones que le da su padre. Esto significa que un Widget usualmente no puede tener cualquier tamaño que quiera.
-
Un Widget no puede conocer y no decide su propia posición en la pantalla, ya que es el padre del Widget quien decide la posición del Widget.
-
Como el tamaño y la posición del padre, a su vez, también dependen de su propio padre, es imposible definir con precisión el tamaño y la posición de cualquier Widget sin tener en cuenta el árbol en su conjunto.
-
Si un hijo quiere un tamaño diferente al de su padre y el padre no tiene suficiente información para alinearlo, entonces el tamaño del hijo podría ignorarse. Sé específico al definir la alineación.
En Flutter, los Widgets son renderizados por sus objetos
RenderBox
subyacentes. Muchas cajas en Flutter,
especialmente aquellas que solo aceptan un hijo,
transmiten su restricción a sus hijos.
En general, hay tres tipos de cajas, en términos de cómo manejan sus restricciones:
- Aquellas que intentan ser lo más grandes posible.
Por ejemplo, las cajas utilizadas por
CenteryListView. - Aquellas que intentan ser del mismo tamaño que sus hijos.
Por ejemplo, las cajas utilizadas por
TransformyOpacity. - Aquellas que intentan ser de un tamaño en particular.
Por ejemplo, las cajas utilizadas por
ImageyText.
Algunos Widgets, por ejemplo Container,
varían de un tipo a otro según los argumentos de su constructor.
El constructor de Container
intenta por defecto
ser lo más grande posible, pero si le das un width,
por ejemplo, intenta respetar eso y tener ese tamaño en particular.
Otros, por ejemplo Row
y Column (cajas flex)
varían según las restricciones que reciben,
como se describe en la sección Flex.
Ejemplos
#Para una experiencia interactiva, usa el siguiente DartPad. Usa la barra de desplazamiento horizontal numerada para cambiar entre 29 ejemplos diferentes.
Si lo prefieres, puedes tomar el código de este repositorio de GitHub.
Los ejemplos se explican en las siguientes secciones.
Ejemplo 1
#
Container(color: red)
La pantalla es el padre del Container, y
fuerza al Container a tener exactamente el mismo tamaño que la pantalla.
Así que el Container llena la pantalla y la pinta de rojo.
Ejemplo 2
#
Container(width: 100, height: 100, color: red)
El Container rojo quiere ser de 100 × 100,
pero no puede, porque la pantalla lo fuerza a tener
exactamente el mismo tamaño que la pantalla.
Así que el Container llena la pantalla.
Ejemplo 3
#
Center(child: Container(width: 100, height: 100, color: red))
La pantalla fuerza al Center a tener exactamente el mismo tamaño
que la pantalla, así que el Center llena la pantalla.
El Center le dice al Container que puede tener cualquier tamaño
que quiera, pero no más grande que la pantalla. Ahora el Container
puede ser efectivamente de 100 × 100.
Ejemplo 4
#
Align(
alignment: Alignment.bottomRight,
child: Container(width: 100, height: 100, color: red),
)
Esto es diferente del ejemplo anterior en que usa
Align en lugar de Center.
Align también le dice al Container que puede tener cualquier tamaño que
quiera, pero si hay espacio vacío no centrará el Container.
En su lugar, alinea el contenedor en la parte inferior derecha del
espacio disponible.
Ejemplo 5
#
Center(
child: Container(
width: double.infinity,
height: double.infinity,
color: red,
),
)
La pantalla fuerza al Center a tener exactamente el
mismo tamaño que la pantalla, así que el Center llena la pantalla.
El Center le dice al Container que puede tener cualquier tamaño que quiera,
pero no más grande que la pantalla. El Container quiere tener un tamaño infinito,
pero como no puede ser más grande que la pantalla,
simplemente llena la pantalla.
Ejemplo 6
#
Center(child: Container(color: red))
La pantalla fuerza al Center a tener exactamente el
mismo tamaño que la pantalla, así que el Center llena la pantalla.
El Center le dice al Container que puede tener cualquier
tamaño que quiera, pero no más grande que la pantalla.
Dado que el Container no tiene hijo ni tamaño fijo,
decide que quiere ser lo más grande posible,
así que llena toda la pantalla.
¿Pero por qué el Container decide eso?
Simplemente porque esa es una decisión de diseño tomada por quienes
crearon el Widget Container. Podría haberse
creado de manera diferente, y tienes que leer la
documentación de la API de Container para entender
cómo se comporta, según las circunstancias.
Ejemplo 7
#
Center(
child: Container(
color: red,
child: Container(color: green, width: 30, height: 30),
),
)
La pantalla fuerza al Center a tener exactamente el mismo
tamaño que la pantalla, así que el Center llena la pantalla.
El Center le dice al Container rojo que puede tener cualquier tamaño
que quiera, pero no más grande que la pantalla. Dado que el
Container rojo no tiene tamaño pero tiene un hijo,
decide que quiere ser del mismo tamaño que su hijo.
El Container rojo le dice a su hijo que puede tener cualquier tamaño
que quiera, pero no más grande que la pantalla.
El hijo es un Container verde que quiere
tener 30 × 30. Dado que el Container rojo se dimensiona a sí mismo al
tamaño de su hijo, también es de 30 × 30.
El color rojo no es visible porque el Container verde
cubre completamente al Container rojo.
Ejemplo 8
#
Center(
child: Container(
padding: const EdgeInsets.all(20),
color: red,
child: Container(color: green, width: 30, height: 30),
),
)
El Container rojo se dimensiona al tamaño de sus hijos,
pero toma en consideración su propio padding.
Así que también es de 30 × 30 más padding.
El color rojo es visible debido al padding,
y el Container verde tiene el mismo tamaño que
en el ejemplo anterior.
Ejemplo 9
#
ConstrainedBox(
constraints: const BoxConstraints(
minWidth: 70,
minHeight: 70,
maxWidth: 150,
maxHeight: 150,
),
child: Container(color: red, width: 10, height: 10),
)
Podrías adivinar que el Container tiene que estar
entre 70 y 150 píxeles, pero estarías equivocado.
El ConstrainedBox solo impone restricciones adicionales
a las que recibe de su padre.
Aquí, la pantalla fuerza al ConstrainedBox a ser exactamente
del mismo tamaño que la pantalla, por lo que le dice a su hijo Container
que también asuma el tamaño de la pantalla, ignorando así su
parámetro constraints.
Ejemplo 10
#
Center(
child: ConstrainedBox(
constraints: const BoxConstraints(
minWidth: 70,
minHeight: 70,
maxWidth: 150,
maxHeight: 150,
),
child: Container(color: red, width: 10, height: 10),
),
)
Ahora, Center permite a ConstrainedBox tener cualquier tamaño hasta
el tamaño de la pantalla. El ConstrainedBox impone restricciones adicionales
de su parámetro constraints sobre su hijo.
El Container debe estar entre 70 y 150 píxeles. Quiere tener 10 píxeles, por lo que termina teniendo 70 (el mínimo).
Ejemplo 11
#
Center(
child: ConstrainedBox(
constraints: const BoxConstraints(
minWidth: 70,
minHeight: 70,
maxWidth: 150,
maxHeight: 150,
),
child: Container(color: red, width: 1000, height: 1000),
),
)
Center permite a ConstrainedBox tener cualquier tamaño hasta el
tamaño de la pantalla. El ConstrainedBox impone restricciones adicionales
de su parámetro constraints sobre su hijo.
El Container debe estar entre 70 y 150 píxeles.
Quiere tener 1000 píxeles,
por lo que termina teniendo 150 (el máximo).
Ejemplo 12
#
Center(
child: ConstrainedBox(
constraints: const BoxConstraints(
minWidth: 70,
minHeight: 70,
maxWidth: 150,
maxHeight: 150,
),
child: Container(color: red, width: 100, height: 100),
),
)
Center permite a ConstrainedBox tener cualquier tamaño hasta el
tamaño de la pantalla. El ConstrainedBox impone restricciones adicionales
de su parámetro constraints sobre su hijo.
El Container debe estar entre 70 y 150 píxeles.
Quiere tener 100 píxeles, y ese es el tamaño que tiene,
ya que está entre 70 y 150.
Ejemplo 13
#
UnconstrainedBox(
child: Container(color: red, width: 20, height: 50),
)
La pantalla fuerza al UnconstrainedBox a tener exactamente
el mismo tamaño que la pantalla. Sin embargo, el UnconstrainedBox
permite que su hijo Container tenga cualquier tamaño que quiera.
Ejemplo 14
#
UnconstrainedBox(
child: Container(color: red, width: 4000, height: 50),
)
La pantalla fuerza al UnconstrainedBox a tener exactamente
el mismo tamaño que la pantalla, y UnconstrainedBox
permite que su hijo Container tenga cualquier tamaño que quiera.
Desafortunadamente, en este caso el Container tiene
4000 píxeles de ancho y es demasiado grande para caber en
el UnconstrainedBox, por lo que el UnconstrainedBox muestra
la muy temida "advertencia de desbordamiento".
Ejemplo 15
#
OverflowBox(
minWidth: 0,
minHeight: 0,
maxWidth: double.infinity,
maxHeight: double.infinity,
child: Container(color: red, width: 4000, height: 50),
)
La pantalla fuerza al OverflowBox a tener exactamente el mismo
tamaño que la pantalla, y OverflowBox permite que su hijo Container
tenga cualquier tamaño que quiera.
OverflowBox es similar a UnconstrainedBox;
la diferencia es que no mostrará ninguna advertencia
si el hijo no cabe en el espacio.
En este caso, el Container tiene 4000 píxeles de ancho,
y es demasiado grande para caber en el OverflowBox,
pero el OverflowBox simplemente muestra tanto como puede,
sin dar ninguna advertencia.
Ejemplo 16
#
UnconstrainedBox(
child: Container(color: Colors.red, width: double.infinity, height: 100),
)
Esto no renderizará nada y verás un error en la consola.
El UnconstrainedBox permite que su hijo tenga cualquier tamaño que quiera,
sin embargo su hijo es un Container con tamaño infinito.
Flutter no puede renderizar tamaños infinitos, por lo que arroja un error con
el siguiente mensaje: BoxConstraints forces an infinite width.
Ejemplo 17
#
UnconstrainedBox(
child: LimitedBox(
maxWidth: 100,
child: Container(
color: Colors.red,
width: double.infinity,
height: 100,
),
),
)
Aquí ya no obtendrás un error,
porque cuando el UnconstrainedBox le da a LimitedBox un
tamaño infinito;
este transmite un ancho máximo de 100 a su hijo.
Si cambias el UnconstrainedBox por un Widget Center,
el LimitedBox ya no aplicará su límite
(ya que su límite solo se aplica cuando recibe restricciones
infinitas), y el ancho del Container
puede crecer más allá de 100.
Esto explica la diferencia entre un LimitedBox
y un ConstrainedBox.
Ejemplo 18
#
const FittedBox(child: Text('Some Example Text.'))
La pantalla fuerza al FittedBox a tener exactamente el mismo
tamaño que la pantalla. El Text tiene cierto ancho natural
(también llamado su ancho intrínseco) que depende de la
cantidad de texto, el tamaño de su fuente, etc.
El FittedBox permite que el Text tenga cualquier tamaño que quiera,
pero después de que el Text le informa su tamaño al FittedBox,
el FittedBox escala el Text hasta que llena todo
el ancho disponible.
Ejemplo 19
#
const Center(child: FittedBox(child: Text('Some Example Text.')))
¿Pero qué pasa si pones el FittedBox dentro de un
Widget Center? El Center permite que el FittedBox
tenga cualquier tamaño que quiera, hasta el tamaño de la pantalla.
El FittedBox luego se dimensiona a sí mismo al Text,
y permite que el Text tenga cualquier tamaño que quiera.
Dado que tanto el FittedBox como el Text tienen el mismo tamaño,
no ocurre ningún escalado.
Ejemplo 20
#
const Center(
child: FittedBox(
child: Text(
'This is some very very very large text that is too big to fit a regular screen in a single line.',
),
),
)
Sin embargo, ¿qué sucede si FittedBox está dentro de un Widget Center,
pero el Text es demasiado grande para caber en la pantalla?
FittedBox intenta dimensionarse a sí mismo al Text,
pero no puede ser más grande que la pantalla.
Luego asume el tamaño de la pantalla,
y redimensiona el Text para que también quepa en la pantalla.
Ejemplo 21
#
const Center(
child: Text(
'This is some very very very large text that is too big to fit a regular screen in a single line.',
),
)
Sin embargo, si eliminas el FittedBox, el Text
obtiene su ancho máximo de la pantalla,
y rompe la línea para que quepa en la pantalla.
Ejemplo 22
#
FittedBox(
child: Container(height: 20, width: double.infinity, color: Colors.red),
)
FittedBox solo puede escalar un Widget acotado
(que tenga ancho y alto no infinitos). De lo contrario,
no renderizará nada,
y verás un error en la consola.
Ejemplo 23
#
Row(
children: [
Container(
color: red,
child: const Text('Hello!', style: big),
),
Container(
color: green,
child: const Text('Goodbye!', style: big),
),
],
)
La pantalla fuerza al Row a tener exactamente el mismo tamaño
que la pantalla.
Al igual que un UnconstrainedBox, el Row no
impondrá ninguna restricción a sus hijos,
y en su lugar permite que tengan cualquier tamaño que quieran.
El Row luego los coloca uno al lado del otro,
y cualquier espacio extra permanece vacío.
Ejemplo 24
#
Row(
children: [
Container(
color: red,
child: const Text(
'This is a very long text that '
'won\'t fit the line.',
style: big,
),
),
Container(
color: green,
child: const Text('Goodbye!', style: big),
),
],
)
Como Row no impondrá ninguna restricción a sus hijos,
es muy posible que los hijos sean demasiado grandes para caber en
el ancho disponible del Row. En este caso, al igual que un
UnconstrainedBox, el Row muestra la "advertencia de desbordamiento".
Ejemplo 25
#
Row(
children: [
Expanded(
child: Center(
child: Container(
color: red,
child: const Text(
'This is a very long text that won\'t fit the line.',
style: big,
),
),
),
),
Container(
color: green,
child: const Text('Goodbye!', style: big),
),
],
)
Cuando el hijo de un Row se envuelve en un Widget Expanded,
el Row ya no permitirá que este hijo defina su propio ancho.
En su lugar, define el ancho del Expanded de acuerdo con los
otros hijos, y solo entonces el Widget Expanded fuerza
al hijo original a tener el ancho del Expanded.
En otras palabras, una vez que usas Expanded,
el ancho del hijo original se vuelve irrelevante y se ignora.
Ejemplo 26
#
Row(
children: [
Expanded(
child: Container(
color: red,
child: const Text(
'This is a very long text that won\'t fit the line.',
style: big,
),
),
),
Expanded(
child: Container(
color: green,
child: const Text('Goodbye!', style: big),
),
),
],
)
Si todos los hijos del Row están envueltos en Widgets Expanded,
cada Expanded tiene un tamaño proporcional a su parámetro flex,
y solo entonces cada Widget Expanded fuerza a su hijo a tener
el ancho del Expanded.
En otras palabras, Expanded ignora el ancho preferido de
sus hijos.
Ejemplo 27
#
Row(
children: [
Flexible(
child: Container(
color: red,
child: const Text(
'This is a very long text that won\'t fit the line.',
style: big,
),
),
),
Flexible(
child: Container(
color: green,
child: const Text('Goodbye!', style: big),
),
),
],
)
La única diferencia si usas Flexible en lugar de Expanded,
es que Flexible permite que su hijo tenga el mismo ancho o uno menor
que el Flexible mismo, mientras que Expanded fuerza
a su hijo a tener exactamente el mismo ancho del Expanded.
Pero tanto Expanded como Flexible ignoran el ancho de sus hijos
al dimensionarse a sí mismos.
Ejemplo 28
#
Scaffold(
body: Container(
color: blue,
child: const Column(children: [Text('Hello!'), Text('Goodbye!')]),
),
)
La pantalla fuerza al Scaffold a tener exactamente el mismo tamaño
que la pantalla, así que el Scaffold llena la pantalla.
El Scaffold le dice al Container que puede tener cualquier tamaño que quiera,
pero no más grande que la pantalla.
Ejemplo 29
#
Scaffold(
body: SizedBox.expand(
child: Container(
color: blue,
child: const Column(children: [Text('Hello!'), Text('Goodbye!')]),
),
),
)
Si quieres que el hijo del Scaffold tenga exactamente el mismo tamaño
que el Scaffold mismo, puedes envolver a su hijo con
SizedBox.expand.
Restricciones estrictas vs laxas
#Es muy común escuchar que alguna restricción es "estricta" o "laxa", entonces ¿qué significa eso?
Restricciones estrictas
#Una restricción estricta ofrece una sola posibilidad, un tamaño exacto. En otras palabras, una restricción estricta tiene su ancho máximo igual a su ancho mínimo; y tiene su altura máxima igual a su altura mínima.
Un ejemplo de esto es el Widget App,
que está contenido por la clase RenderView:
a la caja utilizada por el hijo devuelto por la función
build
de la aplicación se le da una restricción
que la fuerza a llenar exactamente el área de contenido de la aplicación
(típicamente, toda la pantalla).
Otro ejemplo: si anidas un grupo de cajas dentro de otras en la raíz del árbol de renderizado de tu aplicación, todas se adaptarán exactamente entre sí, forzadas por las restricciones strictly estrictas de la caja.
Si vas al archivo box.dart de Flutter y buscas
los constructores de BoxConstraints,
encontrarás lo siguiente:
BoxConstraints.tight(Size size)
: minWidth = size.width,
maxWidth = size.width,
minHeight = size.height,
maxHeight = size.height;
Si vuelves a visitar el Ejemplo 2,
la pantalla fuerza al Container rojo a ser
exactamente del mismo tamaño que la pantalla.
La pantalla logra eso, por supuesto, pasando restricciones estrictas
al Container.
Restricciones laxas
#Una restricción laxa es aquella que tiene un mínimo de cero y un máximo diferente de cero.
Algunas cajas relajan las restricciones entrantes, lo que significa que el máximo se mantiene pero el mínimo se elimina, por lo que el Widget puede tener un ancho y alto mínimo ambos iguales a cero.
En última instancia, el propósito de Center es transformar
las restricciones estrictas que recibió de su padre
(la pantalla) en restricciones laxas para su hijo
(el Container).
Si vuelves a visitar el Ejemplo 3,
el Center permite que el Container rojo sea más pequeño,
pero no más grande que la pantalla.
Restricciones sin límites
#En ciertas situaciones,
la restricción de una caja es sin límites, o infinita.
Esto significa que ya sea el ancho máximo o
la altura máxima está configurada en double.infinity.
Una caja que intenta ser lo más grande posible no funcionará de manera útil cuando reciba una restricción sin límites y, en modo de depuración, arrojará una excepción.
El caso más común donde una caja de renderizado termina
con una restricción sin límites es dentro de una caja flex
(Row
o Column),
y dentro de una región con desplazamiento
(como ListView
y otras subclases de ScrollView).
ListView, por ejemplo,
intenta expandirse para adaptarse al espacio disponible
en su dirección cruzada
(quizás es un bloque que se desplaza verticalmente e
intenta ser tan ancho como su padre).
Si anidas un ListView que se desplaza verticalmente
dentro de un ListView que se desplaza horizontalmente,
la lista interna intenta ser tan ancha como sea posible,
lo cual es infinitamente ancha,
ya que la externa es desplazable en esa dirección.
La siguiente sección describe el error que podrías
encontrar con restricciones sin límites en un Widget Flex.
Flex
#Una caja flex (Row y
Column) se comporta
de manera diferente dependiendo de si su
restricción está acotada o sin límites en
su dirección primaria.
Una caja flex con una restricción acotada en su dirección primaria intenta ser lo más grande posible.
Una caja flex con una restricción sin límites
en su dirección primaria intenta adaptar sus hijos
a ese espacio. El valor flex de cada hijo debe ser
configurado en cero, lo que significa que no puedes usar
Expanded
cuando la caja flex está dentro de
otra caja flex o de un elemento desplazable;
de lo contrario, arroja una excepción.
La dirección cruzada
(ancho para Column
o altura para Row),
nunca debe estar sin límites,
o no podrá alinear razonablemente a sus hijos.
Aprender las reglas de layout para Widgets específicos
#Conocer la regla general de layout es necesario, pero no es suficiente.
Cada Widget tiene mucha libertad al aplicar la regla general, so no hay forma de saber cómo se comporta simplemente leyendo el nombre del Widget.
Si intentas adivinar, probablemente adivines mal. No puedes saber exactamente cómo se comporta un Widget a menos que hayas leído su documentación o estudiado su código fuente.
El código fuente del layout suele ser complejo, por lo que probablemente sea mejor leer simplemente la documentación. Sin embargo, si decides estudiar el código fuente del layout, puedes encontrarlo fácilmente usando las capacidades de navegación de tu IDE.
Aquí hay un ejemplo:
-
Encuentra un
Columnen tu código y navega a su código fuente. Para hacer esto, usacommand+B(macOS) ocontrol+B(Windows/Linux) en Android Studio o IntelliJ. Serás llevado al archivobasic.dart. ComoColumnextiende deFlex, navega al código fuente deFlex(también enbasic.dart). -
Desplázate hacia abajo hasta encontrar un método llamado
createRenderObject(). Como puedes ver, este método devuelve unRenderFlex. Este es el objeto de renderizado para elColumn. Ahora navega al código fuente deRenderFlex, lo que te lleva al archivoflex.dart. -
Desplázate hacia abajo hasta encontrar un método llamado
performLayout(). Este es el método que realiza el layout para elColumn.
Artículo original por Marcelo Glasberg
Marcelo publicó originalmente este contenido como Flutter: La regla de layout avanzada que incluso los principiantes deben saber en Medium. Nos encantó y le pedimos que nos permitiera publicarlo en docs.flutter.dev, a lo cual accedió amablemente. ¡Gracias, Marcelo! Puedes encontrar a Marcelo en GitHub y pub.dev.
Además, gracias a Simon Lightfoot por crear la imagen de cabecera en la parte superior del artículo.
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.