Saltar al contenido principal

Construir interfaces de usuario con Flutter

Introducción al desarrollo de interfaces de usuario en Flutter.

Los widgets de Flutter se construyen utilizando un framework moderno que se inspira en React. La idea central es que construyes tu interfaz de usuario a partir de widgets. Los widgets describen cómo debería lucir su vista dada su configuración y State actuales. Cuando el State de un widget cambia, el widget reconstruye su descripción, la cual el framework compara con la descripción anterior para determinar los cambios mínimos necesarios en el árbol de renderizado subyacente para pasar de un estado al siguiente.

Hola mundo

#

La app mínima de Flutter simplemente llama a la función runApp() con un Widget:

La función runApp() toma el Widget dado y lo convierte en la raíz del árbol de widgets. En este ejemplo, el árbol de widgets consta de dos widgets, el widget Center y su hijo, el widget Text . El framework obliga al widget raíz a cubrir la pantalla, lo que significa que el texto "Hola, mundo" termina centrado en la pantalla. La dirección del texto debe especificarse en este caso; cuando se utiliza el widget MaterialApp, esto se soluciona automáticamente, como se demuestra más adelante.

Al escribir una app, comúnmente crearás nuevos widgets que sean subclases de StatelessWidget o StatefulWidget, dependiendo de si tu widget gestiona algún State. El trabajo principal de un widget es implementar una función build() , que describe el widget en términos de otros widgets de más bajo nivel. El framework construye esos widgets a su vez hasta que el proceso desemboca en widgets que representan el RenderObject subyacente, el cual calcula y describe la geometría del widget.

Widgets básicos

#

Flutter viene con una suite de potentes widgets básicos, de los cuales los siguientes son comúnmente utilizados:

Text

El widget Text te permite crear un fragmento de texto con estilo dentro de tu aplicación.

Row , Column

Estos widgets flexibles te permiten crear layouts flexibles en tanto en dirección horizontal (Row) como vertical (Column). El diseño de estos objetos se basa en el modelo de layout flexbox de la web.

Stack

En lugar de estar orientado linealmente (ya sea horizontal o verticalmente), un widget Stack te permite colocar widgets uno encima de otro en orden de pintado. Luego puedes usar el widget Positioned en los hijos de un Stack para posicionarlos en relación con el borde superior, derecho, inferior o izquierdo del stack. Los Stacks se basan en el modelo de layout de posicionamiento absoluto de la web.

Container

El widget Container te permite crear un elemento visual rectangular. Un contenedor se puede decorar con un BoxDecoration, como un fondo, un borde o una sombra. Un Container también puede tener márgenes, padding y restricciones aplicadas a su tamaño. Además, un Container se puede transformar en el espacio tridimensional mediante una matriz.

A continuación se muestran algunos widgets simples que combinan estos y otros widgets:

Asegúrate de tener una entrada uses-material-design: true en la sección flutter de tu archivo pubspec.yaml. Te permite usar el conjunto predefinido de iconos de Material. Por lo general, es una buena idea incluir esta línea si estás utilizando la librería Material.

yaml
name: my_app
flutter:
  uses-material-design: true

Muchos widgets de Material Design necesitan estar dentro de un MaterialApp para mostrarse correctamente, con el fin de heredar los datos del tema. Por lo tanto, ejecuta la aplicación con un MaterialApp.

El widget MyAppBar crea un Container con una altura de 56 píxeles independientes de la pantalla con un padding interno de 8 píxeles, tanto a la izquierda como a la derecha. Dentro del contenedor, MyAppBar utiliza un layout Row para organizar a sus hijos. El hijo central, el widget title, está marcado como Expanded, lo que significa que se expande para llenar cualquier espacio disponible restante que no haya sido consumido por los otros hijos. Puedes tener múltiples hijos Expanded y determinar la proporción en la que consumen el espacio disponible utilizando el argumento flex de Expanded.

El widget MyScaffold organiza a sus hijos en una columna vertical. En la parte superior de la columna coloca una instancia de MyAppBar, pasando a la barra de la app un widget Text para usar como su título. Pasar widgets como argumentos a otros widgets es una técnica potente que te permite crear widgets genéricos que se pueden reutilizar de muchas maneras. Finalmente, MyScaffold usa un Expanded para llenar el espacio restante con su cuerpo, que consiste en un mensaje centrado.

Para más información, consulta Layouts.

Uso de componentes Material

#

Flutter proporciona una serie de widgets que te ayudan a construir apps que siguen Material Design. Una app Material comienza con el widget MaterialApp , que construye una serie de widgets útiles en la raíz de tu app, incluido un Navigator, que gestiona una pila de widgets identificados por cadenas de texto, también conocidas como "rutas". El Navigator te permite transicionar suavemente entre pantallas de tu aplicación. El uso del widget MaterialApp es totalmente opcional pero es una buena práctica.

Ahora que el código ha cambiado de MyAppBar y MyScaffold a los widgets AppBar y Scaffold , y desde material.dart, la app empieza a verse un poco más Material. Por ejemplo, la barra de la app tiene una sombra y el texto del título hereda el estilo correcto automáticamente. También se añade un botón de acción flotante (floating action button).

Observa que los widgets se pasan como argumentos a otros widgets. El widget Scaffold toma una serie de widgets diferentes como argumentos con nombre, cada uno de los cuales se coloca en el layout de Scaffold en el lugar adecuado. Del mismo modo, el widget AppBar te permite pasar widgets para el widget leading , y para las actions del widget title . Este patrón se repite en todo el framework y es algo que podrías considerar al diseñar tus propios widgets.

Para más información, consulta Widgets de Componentes Material.

Manejo de gestos

#

La mayoría de las aplicaciones incluyen alguna forma de interacción del usuario con el sistema. El primer paso para construir una aplicación interactiva es detectar los gestos de entrada. Mira cómo funciona creando un botón simple:

El widget GestureDetector no tiene una representación visual, sino que detecta los gestos realizados por el usuario. Cuando el usuario toca el Container, el GestureDetector llama a su callback onTap() , en este caso imprimiendo un mensaje en la consola. Puedes usar GestureDetector para detectar una variedad de gestos de entrada, incluidos toques, arrastres y escalados.

Muchos widgets usan un GestureDetector para proporcionar callbacks opcionales para otros widgets. Por ejemplo, los widgets IconButton, ElevatedButton, y FloatingActionButton tienen callbacks onPressed() que se activan cuando el usuario toca el widget.

Para más información, consulta Gestos en Flutter.

Cambiar widgets en respuesta a la entrada

#

Hasta ahora, esta página solo ha utilizado widgets stateless. Los widgets stateless reciben argumentos de su widget padre, los cuales almacenan en variables miembro final . Cuando se le pide a un widget que ejecute build(), utiliza estos valores almacenados para derivar nuevos argumentos para los widgets que crea.

Para construir experiencias más complejas (por ejemplo, para reaccionar de formas más interesantes a la entrada del usuario), las aplicaciones normalmente conservan algún estado. Flutter utiliza StatefulWidgets para capturar esta idea. Los StatefulWidgets son widgets especiales que saben cómo generar objetos State, que luego se utilizan para mantener el estado. Considera este ejemplo básico, utilizando el ElevatedButton mencionado anteriormente:

Te preguntarás por qué StatefulWidget y State son objetos separados. En Flutter, estos dos tipos de objetos tienen ciclos de vida diferentes. Los Widgets son objetos temporales, utilizados para construir una presentación de la aplicación en su estado actual. Los objetos State, por otro lado, son persistentes entre llamadas a build(), lo que les permite recordar información.

El ejemplo anterior acepta la entrada del usuario y utiliza directamente el resultado en su método build(). En aplicaciones más complejas, diferentes partes de la jerarquía de widgets podrían ser responsables de diferentes tareas; por ejemplo, un widget podría presentar una interfaz de usuario compleja con el objetivo de recopilar información específica, como una fecha o ubicación, mientras que otro widget podría usar esa información para cambiar la presentación general.

En Flutter, las notificaciones de cambio fluyen "hacia arriba" en la jerarquía de widgets mediante callbacks, mientras que el estado actual fluye "hacia abajo" hacia los widgets stateless que hacen la presentación. El padre común que redirige este flujo es el State. El siguiente ejemplo un poco más complejo muestra cómo funciona esto en la práctica:

Observa la creación de dos nuevos widgets stateless, separando claramente las responsabilidades de mostrar el contador (CounterDisplay) e incrementar el contador (CounterIncrementor). Aunque el resultado neto es el mismo que en el ejemplo anterior, la separación de responsabilidades permite encapsular una mayor complejidad en los widgets individuales, manteniendo la simplicidad en el padre.

Para obtener más información, consulta:

Integrando todo

#

A continuación se presenta un ejemplo más completo que reúne estos conceptos: Una hipotética aplicación de compras muestra varios productos ofrecidos para la venta y mantiene un carrito de compras para las compras previstas. Comienza definiendo la clase de presentación, ShoppingListItem:

El widget ShoppingListItem sigue un patrón común para widgets stateless. Almacena los valores que recibe en su constructor en variables miembro final , que luego utiliza durante su función build() . Por ejemplo, el booleano inCart conmuta entre dos apariencias visuales: una que utiliza el color primario del tema actual y otra que utiliza gris.

Cuando el usuario toca el elemento de la lista, el widget no modifica su valor inCart directamente. En su lugar, el widget llama a la función onCartChanged que recibió de su widget padre. Este patrón te permite almacenar el estado más arriba en la jerarquía de widgets, lo que hace que el estado persista durante períodos de tiempo más largos. En el caso extremo, el estado almacenado en el widget pasado a runApp() persiste durante toda la vida útil de la aplicación.

Cuando el padre recibe la devolución de llamada onCartChanged, el padre actualiza su estado interno, lo que activa al padre para reconstruir y crear una nueva instancia de ShoppingListItem con el nuevo valor de inCart. Aunque el padre crea una nueva instancia de ShoppingListItem cuando se reconstruye, esa operación es económica porque el framework compara los widgets recién construidos con los previamente construidos y aplica solo las diferencias al RenderObject subyacente.

Aquí hay un ejemplo de widget padre que almacena un estado mutable:

La clase ShoppingList extiende de StatefulWidget, lo que significa que este widget almacena un estado mutable. Cuando el widget ShoppingList se inserta por primera vez en el árbol, el framework llama a la función createState() para crear una nueva instancia de _ShoppingListState que se asociará con esa ubicación en el árbol. (Observa que las subclases de State suelen nombrarse con guiones bajos iniciales para indicar que son detalles de implementación privados). Cuando el padre de este widget se reconstruye, el padre crea una nueva instancia de ShoppingList, pero el framework reutiliza la instancia de _ShoppingListState que ya está en el árbol en lugar de llamar a createState nuevamente.

Para acceder a las propiedades del ShoppingList actual, el _ShoppingListState puede usar su propiedad widget . Si el padre se reconstruye y crea un nuevo ShoppingList, el _ShoppingListState se reconstruye con el nuevo valor de widget. Si deseas ser notificado cuando la propiedad widget cambie, sobrescribe la función didUpdateWidget() , a la que se le pasa un oldWidget para permitirte comparar el widget antiguo con el widget actual.

Al manejar la devolución de llamada onCartChanged, el _ShoppingListState muta su estado interno agregando o eliminando un producto de _shoppingCart. Para señalar al framework que ha cambiado su estado interno, envuelve esas llamadas en una llamada a setState() . Llamar a setState marca este widget como sucio (dirty) y programa su reconstrucción la próxima vez que tu app necesite actualizar la pantalla. Si olvidas llamar a setState al modificar el estado interno de un widget, el framework no sabrá que tu widget está sucio y podría no llamar a la función build() del widget, lo que significa que la interfaz de usuario podría no actualizarse para reflejar el estado cambiado. Al gestionar el estado de esta manera, no necesitas escribir código separado para crear y actualizar widgets hijos. En su lugar, simplemente implementas la función build, que maneja ambas situaciones.

Responder a eventos del ciclo de vida del widget

#

Después de llamar a createState() en el StatefulWidget, el framework inserta el nuevo objeto de estado en el árbol y luego llama a initState() en el objeto de estado. Una subclase de State puede sobrescribir initState para realizar un trabajo que solo necesita ocurrir una vez. Por ejemplo, sobrescribe initState para configurar animaciones o para suscribirte a servicios de la plataforma. Las implementaciones de initState deben comenzar llamando a super.initState.

Cuando un objeto de estado ya no es necesario, el framework llama a dispose() en el objeto de estado. Sobrescribe la función dispose para realizar tareas de limpieza. Por ejemplo, sobrescribe dispose para cancelar temporizadores o para cancelar la suscripción a los servicios de la plataforma. Las implementaciones de dispose típicamente terminan llamando a super.dispose.

Para obtener más información, consulta State.

Keys

#

Usa keys para controlar qué widgets empareja el framework con otros widgets cuando un widget se reconstruye. Por defecto, el framework empareja widgets en la compilación actual y anterior de acuerdo con su runtimeType y el orden en que aparecen. Con las keys, el framework requiere que los dos widgets tengan la misma key así como el mismo runtimeType.

Las keys son más útiles en widgets que construyen muchas instancias del mismo tipo de widget. Por ejemplo, el widget ShoppingList, que construye solo las instancias suficientes de ShoppingListItem para llenar su región visible:

  • Sin keys, la primera entrada en la compilación actual siempre se sincronizaría con la primera entrada en la compilación anterior, incluso si, semánticamente, la primera entrada en la lista simplemente se desplazó fuera de la pantalla y ya no es visible en el viewport.

  • Al asignar a cada entrada de la lista una key "semántica", la lista infinita puede ser más eficiente porque el framework sincroniza entradas con keys semánticas coincidentes y, por lo tanto, apariciones visuales similares (o idénticas). Además, sincronizar las entradas semánticamente significa que el estado retenido en widgets hijos stateful permanece adjunto a la misma entrada semántica en lugar de a la entrada en la misma posición numérica en el viewport.

Para más información, consulta la API de Key .

Global keys

#

Usa global keys para identificar de manera única los widgets hijos. Las global keys deben ser globalmente únicas en toda la jerarquía de widgets, a diferencia de las local keys que solo necesitan ser únicas entre hermanos. Debido a que son globalmente únicas, se puede usar una global key para recuperar el estado asociado con un widget.

Para más información, consulta la API de GlobalKey .