Cómo crear IU atractivas con Flutter

Flutter es el kit de herramientas de IU de Google diseñado para crear aplicaciones atractivas compiladas de forma nativa que funcionen en dispositivos móviles, la Web y computadoras de escritorio a partir de una base de código única. En este codelab, crearás una aplicación de chat simple para Android, iOS y, opcionalmente, la Web.

En este codelab, se brinda información más detallada acerca de Flutter que la que puedes encontrar en Cómo escribir tu primera app de Flutter, parte 1 y parte 2. Si necesitas una introducción más básica a Flutter, comienza con eso.

Qué aprenderás

  • Cómo escribir una app de Flutter que se vea natural tanto en iOS como en Android
  • Cómo usar el IDE de Android Studio, con muchas combinaciones de teclas compatibles con el complemento de Flutter para IntelliJ y Android Studio
  • Cómo depurar tu app de Flutter
  • Cómo ejecutar tu app de Flutter en un emulador, un simulador y un dispositivo

¿Qué te gustaría aprender de este codelab?

Soy nuevo en el tema y me gustaría ver una buena descripción general. Tengo algunos conocimientos sobre este tema, pero me gustaría repasarlo. Estoy buscando código de ejemplo para usar en mi proyecto. Estoy buscando una explicación sobre un tema específico.

Para completar este codelab, necesitas dos tipos de software: el SDK de Flutter (descárgalo) y un editor (configúralo). En este codelab, se supone que usas Android Studio, pero puedes usar tu editor preferido.

Puedes ejecutar este codelab con cualquiera de los siguientes dispositivos:

Si usas Android, debes realizar algunas configuraciones en Android Studio. Si ejecutas la app en iOS, también debes tener instalado Xcode en Mac. Para obtener más información, consulta Configura un editor.

Crea una app de Flutter simple a partir de una plantilla. Luego, modifica la app inicial para crear la app final.

b2f84ff91b0e1396.pngInicia Android Studio.

  1. Si no tienes proyectos abiertos, selecciona Start a new Flutter app en la página de bienvenida. De lo contrario, selecciona File > New > New Flutter Project.
  2. Selecciona Flutter Application como el tipo de proyecto y haz clic en Next.
  3. Verifica que la ruta del SDK de Flutter especifique la ubicación del SDK. Si el campo de texto está en blanco, selecciona Install SDK.
  4. Ingresa friendly_chat como el nombre del proyecto y haz clic en Next.
  5. Usa el nombre de paquete predeterminado que sugiere Android Studio y haz clic en Next.
  6. Haz clic en Finish.
  7. Espera a que Android Studio instale el SDK y cree el proyecto.

b2f84ff91b0e1396.pngTambién puedes crear una app de Flutter en la línea de comandos.

$ flutter create friendly_chat
$ cd friendly_chat
$ dart migrate --apply-changes
$ flutter run

¿Tienes problemas?

Consulta la página Test drive para obtener más información sobre cómo crear una app simple a partir de una plantilla. También puedes usar el código de los siguientes vínculos para realizar un seguimiento continuo.

En esta sección, comenzarás a modificar la app de ejemplo predeterminada para convertirla en una app de chat. El objetivo es usar Flutter para crear FriendlyChat, una app de chat sencilla y extensible con las siguientes funciones:

  • La app muestra mensajes de texto en tiempo real.
  • Los usuarios pueden ingresar un mensaje de string de texto y presionar la tecla Intro o el botón Enviar para enviarlo.
  • La IU se ejecuta en dispositivos iOS y Android, y en la Web.

Prueba la app finalizada en DartPad

Crea la estructura principal de la app

El primer elemento que debes agregar es una barra de la aplicación simple que muestra un título estático para la app. A medida que avances por las siguientes secciones de este codelab, agregarás elementos de IU con más capacidad y respuesta de manera incremental a la app.

El archivo main.dart se encuentra en el directorio lib del proyecto de Flutter y contiene la función main() que inicia la ejecución de tu app.

b2f84ff91b0e1396.pngReemplaza todo el código en main.dart con lo siguiente:

import 'package:flutter/material.dart';

void main() {
  runApp(
    MaterialApp(
      title: 'FriendlyChat',
      home: Scaffold(
        appBar: AppBar(
          title: Text('FriendlyChat'),
        ),
      ),
    ),
  );
}

cf1e10b838bf60ee.png Observaciones

  • Todos los programas de Dart, ya sea una app de línea de comandos, una app de AngularDart o una app de Flutter, comienzan con una función main().
  • Las definiciones de la función main() y runApp() son las mismas que en la app generada automáticamente.
  • La función runApp() toma como argumento un elemento Widget, que el marco de trabajo de Flutter expande y muestra en la pantalla durante el tiempo de ejecución.
  • Esta app de chat usa elementos de Material Design en la IU, por lo que se crea un objeto MaterialApp y se pasa a la función runApp(). El widget MaterialApp se convierte en la raíz del árbol de widgets de tu app.
  • El argumento home especifica la pantalla predeterminada que los usuarios ven en tu app. En este caso, consiste de un widget Scaffold que tiene un elemento AppBar simple como widget secundario. Esto es normal para una app de Material.

b2f84ff91b0e1396.pngPara ejecutar la app, haz clic en el ícono Ejecutar 6869d41b089cc745.png del editor. La primera vez que ejecutas una app, puede tardar un poco. La app es más rápida en los pasos posteriores.

febbb7a3b70462b7.png

Deberías ver un resultado similar al siguiente:

Pixel 3 XL

iPhone 11

Compila la pantalla del chat

Para establecer las bases de los componentes interactivos, divide la app simple en dos subclases diferentes de widget: un widget de nivel raíz FriendlyChatApp que nunca cambia y un widget ChatScreen secundario que se vuelve a compilar cuando se envían mensajes y cambios de estado interno. Por ahora, ambas clases pueden extender StatelessWidget. Luego, modificarás ChatScreen para que sea un widget con estado. De esta manera, puedes cambiar su estado según sea necesario.

b2f84ff91b0e1396.pngCrea el widget FriendlyChatApp:

  1. En main(), coloca el cursor delante de M en MaterialApp.
  2. Haz clic con el botón derecho y selecciona Refactor > Extract > Extract Flutter widget.

a133a9648f86738.png

  1. Ingresa FriendlyChatApp en el diálogo ExtractWidget y haz clic en el botón Refactor. El código MaterialApp se coloca en un nuevo widget sin estado llamado FriendlyChatApp, y main() se actualiza para llamar a esa clase cuando llama a la función runApp().
  2. Selecciona el bloque de texto después de home:. Comienza con Scaffold( y termina con el paréntesis de cierre de Scaffold, ). No incluyas la coma final.
  3. Comienza a escribir ChatScreen, y selecciona ChatScreen() en la ventana emergente. (Elige la entrada ChatScreen que esté marcada con un signo igual dentro del círculo amarillo. Esto te proporciona una clase con paréntesis vacíos, en lugar de una constante).

b2f84ff91b0e1396.pngCrea un widget sin estado, ChatScreen:

  1. En la clase FriendlyChatApp, alrededor de la línea 27, comienza a escribir stless. En el editor, se te preguntará si deseas crear un widget Stateless. Presiona Intro para aceptar. Aparece el código estándar y el cursor se posiciona para que ingreses el nombre de tu widget sin estado.
  2. Ingresa ChatScreen.

b2f84ff91b0e1396.pngActualiza el widget ChatScreen:

  1. Dentro del widget ChatScreen, selecciona Container y comienza a escribir Scaffold. Selecciona Scaffold en la ventana emergente.
  2. El cursor debe posicionarse entre paréntesis. Presiona Intro para iniciar una nueva línea.
  3. Comienza a escribir appBar, y selecciona appBar: en la ventana emergente.
  4. Después de appBar:, comienza a escribir AppBar, y selecciona la clase AppBar en la ventana emergente.
  5. Dentro de los paréntesis, comienza a escribir title, y selecciona title: en la ventana emergente.
  6. Después de title:, comienza a escribir Text, y selecciona la clase Text.
  7. El código estándar para Text contiene la palabra data. Borra la primera coma después de data. Selecciona data, y reemplázalo por 'FriendlyChat'. (Dart admite comillas simples o dobles, pero prefiere comillas simples, a menos que el texto ya contenga una comilla simple).

Observa la esquina superior derecha del panel de código. Si ves una marca de verificación verde, quiere decir que tu código aprobó el análisis. ¡Felicitaciones!

cf1e10b838bf60ee.png Observaciones

En este paso, se presentan varios conceptos clave del marco de trabajo de Flutter:

  • Describe la parte de la interfaz de usuario representada por un widget en su método build(). El marco de trabajo llama a los métodos build() para FriendlyChatApp y ChatScreen cuando inserta estos widgets en la jerarquía del widget y cuando cambian sus dependencias.
  • @override es una anotación de Dart que indica que el método etiquetado anula el método de una superclase.
  • Algunos widgets, como Scaffold y AppBar, son específicos de las apps de Material Design. Otros widgets, como Text, son genéricos y se pueden usar en cualquier app. Los widgets de diferentes bibliotecas del marco de trabajo de Flutter son compatibles y pueden funcionar en conjunto en una sola aplicación.
  • La simplificación del método main() permite la carga en caliente porque la misma no vuelve a ejecutar main().

b2f84ff91b0e1396.pngHaz clic en el botón 48583acd5d1a5e12.png de recarga en caliente para ver los cambios casi al instante. Tras dividir la IU en clases separadas y modificar el widget raíz, no deberías observar ningún cambio visible en la IU.

¿Tienes problemas?

Si tu app no se ejecuta correctamente, comprueba que no haya errores ortográficos. Si es necesario, usa el código que aparece en el siguiente vínculo para volver a empezar.

En esta sección, aprenderás a crear un control de usuario que le permita ingresar y enviar mensajes de chat.

64fd9c97437a7461.png

En un dispositivo, cuando se hace clic en el campo de texto aparece un teclado en pantalla. Los usuarios pueden enviar mensajes de chat. Para ello, pueden escribir una string con contenido y presionar la tecla Intro en el teclado en pantalla. Como alternativa, los usuarios pueden enviar los mensajes escritos si presionan el botón gráfico Enviar junto al campo de entrada.

Por ahora, la IU para redactar mensajes se encuentra en la parte superior de la pantalla de chat, pero después de agregar la IU para mostrar los mensajes en el siguiente paso, deberás moverla a la parte inferior de la pantalla de chat.

Cómo agregar un campo de entrada de texto interactivo

El marco de trabajo de Flutter proporciona un widget de Material Design llamado TextField. Es un StatefulWidget (un widget con estado mutable) con propiedades para personalizar el comportamiento del campo de entrada. State es información que se puede leer de manera síncrona cuando se compila el widget y puede cambiar durante la vida útil del widget. Para agregar el primer widget con estado a la app de FriendlyChat, deberás realizar algunas modificaciones.

b2f84ff91b0e1396.pngCambia la clase ChatScreen para que tenga estado:

  1. Selecciona ChatScreen en la línea class ChatScreen extends StatelessWidget.
  2. Presiona Option+Return (macOS) o Alt+Enter (Linux y Windows) para que aparezca el menú.
  3. En el menú, selecciona Convert to StatefulWidget. La clase se actualiza automáticamente con el código estándar para un widget con estado, incluida una nueva clase _ChatScreenState para administrar el estado.

Para administrar las interacciones con el campo de texto, usa un objeto TextEditingController para leer el contenido del campo de entrada y borrar el campo después del mensaje de chat.

b2f84ff91b0e1396.pngAgrega un elemento TextEditingController a _ChatScreenState.

Agrega lo siguiente como primera línea en la clase _ChatScreenState:

final _textController = TextEditingController();

Ahora que tu app puede administrar el estado, puedes compilar la clase _ChatScreenState con un campo de entrada y un botón Enviar.

b2f84ff91b0e1396.pngAgrega una función _buildTextComposer a _ChatScreenState:

  Widget _buildTextComposer() {
    return  Container(
        margin: EdgeInsets.symmetric(horizontal: 8.0),
      child: TextField(
        controller: _textController,
        onSubmitted: _handleSubmitted,
        decoration: InputDecoration.collapsed(
            hintText: 'Send a message'),
      ),
    );
  }

cf1e10b838bf60ee.png Observaciones

  • En Flutter, los datos con estado de un widget se encapsulan en un objeto State. Luego, el objeto State se asocia con un widget que extiende la clase StatefulWidget.
  • El código anterior define un método privado llamado _buildTextComposer() que muestra un widget Container con un widget TextField configurado.
  • El widget Container agrega un margen horizontal entre el borde de la pantalla y cada lado del campo de entrada.
  • Las unidades que se pasan a EdgeInsets.symmetric son píxeles lógicos que se traducen en una cantidad específica de píxeles físicos, según la proporción de píxeles de un dispositivo. Es posible que estés familiarizado con el término equivalente para Android (píxeles independientes de la densidad) o para iOS (puntos).
  • La propiedad onSubmitted proporciona un método de devolución de llamada privado, _handleSubmitted(). Al principio, este método solo borra el campo, pero luego lo extiende para enviar el mensaje de chat.
  • El elemento TextField con el elemento TextEditingController te permite controlar el campo de texto. Este controlador borrará el campo y leerá su valor.

b2f84ff91b0e1396.pngAgrega la función _handleSubmitted a _ChatScreenState para borrar el controlador de texto:

  void _handleSubmitted(String text) {
    _textController.clear();
  }

Cómo agregar un widget de compositor de texto

b2f84ff91b0e1396.pngActualiza el método build() para _ChatScreenState.

Después de la línea appBar: AppBar(...), agrega una propiedad body::

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: Text('FriendlyChat')),
      body: _buildTextComposer(),    // NEW
    );
  }

cf1e10b838bf60ee.png Observaciones

  • El método _buildTextComposer muestra un widget que encapsula el campo de entrada de texto.
  • Si se agrega _buildTextComposer a la propiedad body, la app muestra el control de texto de usuario de entrada de texto.

b2f84ff91b0e1396.pngVuelve a cargar la app en caliente. Deberías ver una pantalla similar a la siguiente:

Pixel 3 XL

iPhone 11

Cómo agregar un botón Enviar responsivo

A continuación, agregarás el botón Enviar a la derecha del campo de texto. Esto implica agregar un poco más de estructura al diseño.

b2f84ff91b0e1396.pngEn la función _buildTextComposer, une TextField a un elemento Row:

  1. Selecciona TextField en _buildTextComposer.
  2. Presiona Option+Return (macOS) o Alt+Enter (Linux y Windows) para que aparezca un menú y selecciona Wrap with widget. Se agrega un nuevo widget que une a TextField. Se selecciona el nombre del marcador de posición y el IDE te espera para que ingreses un nuevo nombre de marcador de posición.
  3. Comienza a escribir Row, y selecciona Row en la lista que aparece. Aparecerá una ventana emergente con la definición del constructor de Row. La propiedad child tiene un borde rojo y el analizador indica que falta la propiedad children obligatoria.
  4. Coloca el cursor sobre child y aparecerá una ventana emergente. En la ventana emergente, se te preguntará si deseas cambiar la propiedad a children. Selecciona esa opción.
  5. La propiedad children toma una lista, en lugar de un solo widget. (Por ahora, solo hay un elemento en la lista, pero podrás agregar otro pronto). Convierte el widget en una lista de un elemento. Para ello, escribe un corchete izquierdo ([) después del texto children:. El editor también proporciona el signo de cierre derecho. Borra el corchete de cierre. Varias líneas hacia abajo, justo antes de las llaves de cierre correctas que cierran la fila, escribe el corchete derecho seguido de una coma (],). El analizador debería mostrar una marca de verificación verde.
  6. Ahora, el código es correcto, pero no tiene el formato adecuado. Haz clic con el botón derecho en el panel de código y selecciona Reformat Code with dartfmt.

b2f84ff91b0e1396.pngUne el elemento TextField dentro de una Flexible:

  1. Selecciona Row.
  2. Presiona Option+Return (macOS) o Alt+Enter (Linux y Windows) para que aparezca un menú y selecciona Wrap with widget. Se agrega un nuevo widget que une a TextField. Se selecciona el nombre del marcador de posición y el IDE te espera para que ingreses un nuevo nombre de marcador de posición.
  3. Comienza a escribir Flexible, y selecciona Flexible en la lista que aparece. Aparecerá una ventana emergente con la definición del constructor de Row.
Widget _buildTextComposer() {
  return  Container(
    margin: EdgeInsets.symmetric(horizontal: 8.0),
    child:  Row(                             // NEW
      children: [                            // NEW
         Flexible(                           // NEW
          child:  TextField(
            controller: _textController,
            onSubmitted: _handleSubmitted,
            decoration:  InputDecoration.collapsed(
                hintText: 'Send a message'),
          ),
        ),                                    // NEW
      ],                                      // NEW
    ),                                        // NEW
  );
}

cf1e10b838bf60ee.png Observaciones

  • Con un elemento Row, puedes colocar el botón Enviar junto al campo de entrada.
  • Al unir el elemento TextField a un widget Flexible se indica a Row que modifique el tamaño del campo de texto de forma automática para usar el espacio restante que no usa el botón.
  • Agregar la coma después del corchete derecho le indica al formateo cómo darle formato al código.

A continuación, debes agregar un botón Enviar. Esta es una app de Material, por lo que debes usar el ícono de Material correspondiente 2de111ba4b057a1e.png:

b2f84ff91b0e1396.pngAgrega el botón Enviar a Row.

El botón Enviar se convierte en el segundo elemento de la lista de Row.

  1. Coloca el cursor al final del paréntesis de cierre y la coma de cierre del widget Flexible y presiona Intro para iniciar una nueva línea.
  2. Comienza a escribir Container, y selecciona Container en la ventana emergente. El cursor se posiciona dentro de los paréntesis del contenedor. Presiona Intro para iniciar una nueva línea.
  3. Agrega las siguientes líneas de código al contenedor:
margin: EdgeInsets.symmetric(horizontal: 4.0),
child: IconButton(
    icon: const Icon(Icons.send),
    onPressed: () => _handleSubmitted(_textController.text)),

cf1e10b838bf60ee.png Observaciones

  • El elemento IconButton muestra el botón Enviar.
  • La propiedad icon especifica la constante Icons.send de la biblioteca de Material para crear una nueva instancia Icon.
  • Colocar el elemento IconButton dentro de un widget Container te permite personalizar el espaciado del margen para que se ajuste mejor a tu campo de entrada.
  • La propiedad onPressed usa una función anónima para invocar el método _handleSubmitted() y pasa el contenido del mensaje con el elemento _textController.
  • En Dart, la sintaxis de la flecha (=> expression) se usa a veces para declarar funciones. Esta es una abreviatura de { return expression; } y solo se usa para funciones de una línea. Para obtener una descripción general de la compatibilidad con funciones de Dart, incluidas las funciones anónimas y anidadas, consulta el Recorrido del lenguaje Dart.

b2f84ff91b0e1396.pngVuelve a cargar la app en caliente para ver el botón Enviar:

Pixel 3 XL

iPhone 11

El color del botón es el negro, que proviene del tema predeterminado de Material Design. Para cambiar el color de los elementos destacados de la app, pasa el argumento de color a IconButton o aplica otro tema.

b2f84ff91b0e1396.pngEn _buildTextComposer(), une el elemento Container con un elemento IconTheme:.

  1. Selecciona Container en la parte superior de la función _buildTextComposer().
  2. Presiona Option+Return (macOS) o Alt+Enter (Linux y Windows) para que aparezca un menú y selecciona Wrap with widget. Se agrega un nuevo widget que une a Container. Se selecciona el nombre del marcador de posición y el IDE te espera para que ingreses un nuevo nombre de marcador de posición.
  3. Comienza a escribir IconTheme, y selecciona IconTheme en la lista. La propiedad child está rodeada por un recuadro rojo, y el analizador te indica que es necesaria la propiedad data.
  4. Agrega la propiedad data:
return IconTheme(
  data: IconThemeData(color: Theme.of(context).accentColor), // NEW
  child: Container(

cf1e10b838bf60ee.png Observaciones

  • Los íconos heredan su color, opacidad y tamaño de un widget IconTheme, que utiliza un objeto IconThemeData para definir estas características.
  • La propiedad IconTheme de data especifica el objeto ThemeData para el tema actual. Esto le da al botón (y a cualquier otro icono de esta parte del árbol de widgets) el color de los elementos destacados del tema actual.
  • Un objeto BuildContext es un controlador para la ubicación de un widget en el árbol de widgets de tu app. Cada widget tiene su propio BuildContext, que se convierte en el elemento principal del widget que muestra la función StatelessWidget.build o State.build. Esto significa que _buildTextComposer() puede acceder al objeto BuildContext desde su objeto encapsulado State. No es necesario pasar el contexto al método de forma explícita.

b2f84ff91b0e1396.pngVuelve a cargar la app en caliente. Ahora, el botón Enviar debería ser azul:

Pixel 3 XL

iPhone 11

¿Tienes problemas?

Si tu app no se ejecuta correctamente, comprueba que no haya errores ortográficos. Si es necesario, usa el código que aparece en el siguiente vínculo para volver a empezar.

e57d18c5bb8f2ac7.png¡Encontraste algo especial!

Existen varias formas de depurar tu app. Puedes usar el IDE directamente para establecer puntos de interrupción o puedes usar las Herramientas para desarrolladores de Dart (no confundir con las Herramientas para desarrolladores de Chrome). En este codelab, se muestra la manera de establecer puntos de interrupción con IntelliJ y Android Studio. Si usas otro editor, como VS Code, usa las Herramientas para desarrolladores para depurar. Si quieres una introducción más detallada sobre las Herramientas para desarrolladores de Dart, consulta el paso 2.5 de Cómo escribir tu primera app de Flutter en la Web.

Los IDE de IntelliJ y Android Studio te permiten depurar apps de Flutter que se ejecutan en un emulador, un simulador o un dispositivo. Con estos editores, puede realizar lo siguiente:

  • Seleccionar un dispositivo o simulador para depurar tu app
  • Ver los mensajes de la consola
  • Establecer puntos de interrupción en tu código
  • Examinar variables y evaluar expresiones en el tiempo de ejecución

Los editores de IntelliJ y Android Studio muestran el registro del sistema mientras se ejecuta tu app y proporciona una IU de Debugger para trabajar con puntos de interrupción y controlar el flujo de ejecución.

6ea611ca007eb43c.png

Cómo trabajar con interrupciones

b2f84ff91b0e1396.pngDepura tu app de Flutter con puntos de interrupción:

  1. Abre el archivo de origen en el que deseas establecer un punto de interrupción.
  2. Busca la línea en la que deseas establecer un punto de interrupción, haz clic en ella y, luego, selecciona Run > Toggle Line Breakpoint. De manera alternativa, puedes hacer clic en el margen (a la derecha del número de línea) para activar o desactivar un punto de interrupción.
  3. Si no estabas ejecutando en modo de depuración, detén la app.
  4. Reinicia la app con Run > Debug o haciendo clic en el botón Run debug en la IU.

El editor inicia la IU de Debugger y pausa la ejecución de tu app cuando alcanza el punto de interrupción. Luego, puedes usar los controles en la IU del depurador para identificar la causa del error.

Practica con el depurador mediante la configuración de puntos de interrupción en los métodos build() en tu app de FriendlyChat y luego ejecuta y depura la app Puedes inspeccionar los marcos de pila para ver el historial de llamadas de método de tu app.

Una vez que hayas implementado la estructura básica de la app y la pantalla, podrás definir el área en la que se muestran los mensajes de chat.

de23b9bb7bf84592.png

Implementa una lista de mensajes de chat

En esta sección, crearás un widget que muestre mensajes de chat mediante composición (creación y combinación de varios widgets más pequeños). Comenzarás con un widget que representa un solo mensaje de chat. Luego, anidarás ese widget en una lista desplazable principal. Por último, anidarás la lista desplazable en el panel básico de la app.

b2f84ff91b0e1396.pngAgrega el widget sin estado ChatMessage:

  1. Coloca el cursor después de la clase FriendlyChatApp y comienza a escribir stless. (El orden de las clases no es importante, pero este orden facilita la comparación de tu código con la solución).
  2. Ingresa ChatMessage para el nombre de la clase.

b2f84ff91b0e1396.pngAgrega un Row al método build() para ChatMessage:

  1. Coloca el cursor dentro de los paréntesis en return Container() y presiona Intro para iniciar una nueva línea.
  2. Agrega una propiedad margin:
margin: EdgeInsets.symmetric(vertical: 10.0),
  1. El elemento secundario de Container' será un elemento Row. La lista de Row contiene dos widgets: un avatar y una columna de texto.
return Container(
  child: Row(
    crossAxisAlignment: CrossAxisAlignment.start,
    children: [
      Container(
        margin: const EdgeInsets.only(right: 16.0),
        child: CircleAvatar(child: Text(_name[0])),
      ),
      Column(
        crossAxisAlignment: CrossAxisAlignment.start,
        children: [
          Text(_name, style: Theme.of(context).textTheme.headline4),
          Container(
            margin: EdgeInsets.only(top: 5.0),
            child: Text(text),
          ),
        ],
      ),
    ],
  ),
);
  1. Agrega una variable text y un constructor en la parte superior de ChatMessage:
class ChatMessage extends StatelessWidget {
  ChatMessage({required this.text}); // NEW
  final String text;                 // NEW

En este punto, el analizador solo debe reclamar sobre _name que no está definido. Eso lo solucionarás más tarde.

b2f84ff91b0e1396.pngDefine la variable _name.

Define la variable _name como se muestra y reemplaza Your Name con tu propio nombre. Puedes usar esta variable para etiquetar cada mensaje de chat con el nombre del remitente. En este codelab, debes codificar el valor por cuestiones de simplicidad, pero la mayoría de las apps recuperan el nombre del remitente mediante la autenticación. Después de la función main(), agrega la siguiente línea:

String _name = 'Your Name';

cf1e10b838bf60ee.png Observaciones

  • El método build() para ChatMessage muestra un Row que muestra un avatar gráfico simple que representa al usuario que envió el mensaje de chat, un Column que contiene el nombre del remitente y el texto del mensaje.
  • Para personalizar CircleAvatar, etiquétalo con la inicial del usuario y pasa el primer carácter del valor de la variable _name a un widget secundario Text.
  • El parámetro crossAxisAlignment especifica CrossAxisAlignment.start en el constructor Row para posicionar el avatar y los mensajes relacionados con sus widgets superiores. Para el avatar, el elemento principal es un widget Row cuyo eje principal es horizontal, de modo que CrossAxisAlignment.start le brinda la posición más alta en el eje vertical. Para los mensajes, el elemento principal es un widget Column cuyo eje principal es vertical, por lo que CrossAxisAlignment.start alinea el texto en la posición más a la izquierda del eje horizontal.
  • Junto al avatar, dos widgets Text se alinean verticalmente para mostrar el nombre del remitente en la parte superior y el texto del mensaje a continuación.
  • Theme.of(context) proporciona el objeto predeterminado ThemeData de Flutter para la app. En un paso posterior, anularás este tema predeterminado para diseñar tu app de manera diferente en iOS y Android.
  • El elemento textTheme de ThemeData te da acceso a estilos lógicos de Material Design para textos como headline4, para que puedas evitar codificar tamaños de fuente y otros atributos de texto. En este ejemplo, el nombre del remitente recibe un estilo que supera el texto del mensaje.

b2f84ff91b0e1396.pngVuelve a cargar la app en caliente.

Escribe los mensajes en el campo de texto. Presiona el botón Enviar para borrar el mensaje. Escribe un mensaje largo en el campo de texto para ver qué sucede cuando el campo de texto se desborda. Más adelante, en el paso 9, une la columna en un widget Expanded para que el widget Text se ajuste.

Implementa una lista de mensajes de chat en la IU

El siguiente perfeccionamiento es obtener la lista de mensajes de chat y mostrarlo en la IU. Esta lista debe ser desplazable, para que los usuarios puedan ver el historial de mensajes. También debe presentar los mensajes en orden cronológico, con el mensaje más reciente en la fila que se encuentra más abajo en la lista visible.

b2f84ff91b0e1396.pngAgrega una lista _messages a _ChatScreenState.

En la definición de _ChatScreenState, agrega un miembro List llamado _messages para representar cada mensaje de chat:

class _ChatScreenState extends State<ChatScreen> {
  final List<ChatMessage> _messages = [];      // NEW
  final _textController = TextEditingController();

b2f84ff91b0e1396.pngModifica el método _handleSubmitted() en _ChatScreenState.

Cuando el usuario envía un mensaje de chat desde el campo de texto, la app debe agregar el nuevo mensaje a la lista de mensajes. Modifica el método _handleSubmitted() para implementar este comportamiento:

void _handleSubmitted(String text) {
  _textController.clear();
  ChatMessage message = ChatMessage(    //NEW
    text: text,                         //NEW
  );                                    //NEW
  setState(() {                         //NEW
    _messages.insert(0, message);       //NEW
  });                                   //NEW
 }

b2f84ff91b0e1396.pngVuelve a enfocar el campo de texto después de enviar el contenido.

  1. Agrega un elemento FocusNode a _ChatScreenState:
class _ChatScreenState extends State<ChatScreen> {
  final List<ChatMessage> _messages = [];
  final _textController = TextEditingController();
  final FocusNode _focusNode = FocusNode();    // NEW
  1. Agrega la propiedad focusNode a TextField en _buildTextComposer():
child: TextField(
  controller: _textController,
  onSubmitted: _handleSubmitted,
  decoration: InputDecoration.collapsed(hintText: 'Send a message'),
  focusNode: _focusNode,  // NEW
),
  1. En _handleSubmitted(), después de la llamada a setState(), solicita el foco en el elemento TextField:
    setState(() {
      _messages.insert(0, message);
    });
    _focusNode.requestFocus();  // NEW

cf1e10b838bf60ee.png Observaciones

  • Cada elemento de la lista es una instancia de ChatMessage.
  • Cuando se inicializa, la lista está vacía.
  • Llamar a setState() para modificar _messages permite que el marco de trabajo sepa que esta parte del árbol de widgets cambió y que debe volver a compilar la IU. Solo se deben realizar operaciones síncronas en setState() porque, de lo contrario, el marco de trabajo podría volver a compilar los widgets antes de que finalice la operación.
  • En general, es posible llamar a setState() con un cierre vacío una vez modificados algunos datos privados fuera de esta llamada al método. Sin embargo, se prefiere actualizar los datos dentro del cierre de setState, para que no te olvides de realizar la llamada luego.

b2f84ff91b0e1396.pngVuelve a cargar la app en caliente.

Ingresa el texto en el campo de texto y presiona Return. Una vez más, el enfoque se encuentra en el campo de texto.

Coloca la lista de mensajes

Ahora, estás listo para mostrar la lista de mensajes de chat. Obtén los widgets ChatMessage de la lista _messages y colócalos en un widget ListView para obtener una lista desplazable.

b2f84ff91b0e1396.pngEn el método build() para _ChatScreenState, agrega un elemento ListView dentro de Column:

Widget build(BuildContext context) {
  return Scaffold(
    appBar: AppBar(title: Text ('FriendlyChat')),
    body: Column(                                            // MODIFIED
      children: [                                            // NEW
        Flexible(                                            // NEW
          child: ListView.builder(                           // NEW
            padding: EdgeInsets.all(8.0),                    // NEW
            reverse: true,                                   // NEW
            itemBuilder: (_, int index) => _messages[index], // NEW
            itemCount: _messages.length,                     // NEW
          ),                                                 // NEW
        ),                                                   // NEW
        Divider(height: 1.0),                                // NEW
        Container(                                           // NEW
          decoration: BoxDecoration(
            color: Theme.of(context).cardColor),             // NEW
          child: _buildTextComposer(),                       // MODIFIED
        ),                                                   // NEW
      ],                                                     // NEW
    ),                                                       // NEW
  );
}

cf1e10b838bf60ee.png Observaciones

  • El método de fábrica ListView.builder compila una lista a pedido mediante una función a la que se llama una vez por elemento en la lista. La función muestra un widget nuevo en cada llamada. El compilador también detecta automáticamente las mutaciones de su parámetro children y, luego, inicia una recompilación.
  • Los parámetros que se pasan al constructor de ListView.builder personalizan el contenido y la apariencia de la lista:
  • padding crea espacios en blanco alrededor del texto del mensaje.
  • itemCount especifica la cantidad de mensajes en la lista.
  • itemBuilder proporciona la función que compila cada widget en [index]. Como no necesitas el contexto de compilación actual, puedes ignorar el primer argumento de IndexedWidgetBuilder. Nombrar el argumento con un guion bajo (_) y nada más es una convención que indica que no se usará el argumento.
  • La propiedad body del widget Scaffold ahora contiene la lista de mensajes entrantes, así como el campo de entrada y el botón Enviar. El diseño utiliza los siguientes widgets:
  • Column: Escala a sus hijos secundarios directamente. El widget Column toma una lista de widgets secundarios (al igual que un Row) que se convierte en una lista desplazable y una fila para un campo de entrada.
  • Flexible, como elemento principal de ListView: Indica al marco de trabajo que permita que se amplíe la lista de mensajes recibidos para ocupar la altura de Column y, al mismo tiempo, TextField se mantiene en un tamaño fijo.
  • Divider: Dibuja una línea horizontal entre la IU para mostrar mensajes y el campo de entrada de texto para redactar mensajes.
  • Container, como elemento principal del compositor de texto: Define imágenes de fondo, relleno, márgenes y otros detalles de diseño comunes.
  • decoration: Crea un nuevo objeto BoxDecoration que define el color de fondo. En este caso, se usa el cardColor definido por el objeto ThemeData del tema predeterminado. Esto permite que la IU para redactar mensajes tenga un fondo diferente al de la lista de mensajes.

b2f84ff91b0e1396.pngVuelve a cargar la app en caliente. Deberías ver una pantalla como la siguiente:

Pixel 3 XL

iPhone 11

b2f84ff91b0e1396.pngIntenta enviar algunos mensajes de chat con las IU para redactar y mostrar lo que acabas de compilar.

Pixel 3 XL

iPhone 11

¿Tienes problemas?

Si tu app no se ejecuta correctamente, comprueba que no haya errores ortográficos. Si es necesario, usa el código que aparece en el siguiente vínculo para volver a empezar.

Puedes agregar animaciones a tus widgets para que la experiencia del usuario de tu app sea más fluida e intuitiva. En esta sección, aprenderás a agregar un efecto de animación básico a tu lista de mensajes de chat.

Cuando el usuario envía un mensaje de chat nuevo, en lugar de simplemente mostrarlo en la lista de mensajes, debes animar el mensaje para que se mueva verticalmente desde la parte inferior de la pantalla.

Las animaciones en Flutter se encapsulan como objetos Animation que contienen un valor escrito y un estado (como forward, reverse, completed y dismissed). Puedes adjuntar un objeto de animación a un widget o detectar cambios en el objeto de animación. Según los cambios en las propiedades del objeto de animación, el marco de trabajo puede modificar la forma en que aparece tu widget y reconstruir el árbol de widgets.

Especifica un controlador de animación

Usa la clase AnimationController para especificar cómo debe ejecutarse la animación. El elemento AnimationController te permite definir características importantes de la animación, como su duración y dirección de reproducción (hacia adelante o a la inversa).

b2f84ff91b0e1396.pngActualiza la definición de clase _ChatScreenState para que incluya un elemento TickerProviderStateMixin:

class _ChatScreenState extends State<ChatScreen> with TickerProviderStateMixin {   // MODIFIED
  final List<ChatMessage> _messages = [];
  final _textController = TextEditingController();
  final FocusNode _focusNode = FocusNode();
  ...

b2f84ff91b0e1396.pngEn la definición de clase ChatMessage, agrega una variable para almacenar el controlador de animación:

class ChatMessage extends StatelessWidget {
  ChatMessage({required this.text, required this.animationController}); // MODIFIED
  final String text;
  final AnimationController animationController;      // NEW
  ...

b2f84ff91b0e1396.pngAgrega un controlador de animación al método _handleSubmitted():

void _handleSubmitted(String text) {
  _textController.clear();
  var message = ChatMessage(
    text: text,
    animationController: AnimationController(      // NEW
      duration: const Duration(milliseconds: 700), // NEW
      vsync: this,                                 // NEW
    ),                                             // NEW
  );                                               // NEW
  setState(() {
    _messages.insert(0, message);
  });
  _focusNode.requestFocus();
  message.animationController.forward();           // NEW
}

cf1e10b838bf60ee.png Observaciones

  • El objeto AnimationController especifica la duración del tiempo de ejecución de la animación para que sea de 700 milisegundos. (Esta duración más larga ralentiza el efecto de la animación para que la transición se realice de forma más gradual. En la práctica, probablemente desees establecer una menor duración cuando ejecutes tu app).
  • El controlador de animación se adjunta a una nueva instancia de ChatMessage y especifica que la animación debe reproducirse hasta que se agregue un mensaje a la lista de chat.
  • Cuando creas un elemento AnimationController, debes pasar un argumento vsync. vsync es la fuente de la señal de monitoreo de funcionamiento (el elemento Ticker) que hace avanzar la animación. En este ejemplo, se usa _ChatScreenState como vsync, por lo que agrega una combinación TickerProviderStateMixin a la definición de la clase _ChatScreenState.
  • En Dart, una combinación permite reutilizar el cuerpo de una clase en varias jerarquías de clases. Para obtener más información, consulta Cómo agregar funciones a una clase: combinaciones, una sección de la Recorrido del lenguaje Dart.

Agrega un widget de SizeTransition

Agregar un widget SizeTransition a la animación tiene el efecto de animar un elemento ClipRect que expone cada vez más el texto a medida que se desliza.

b2f84ff91b0e1396.pngAgrega un widget SizeTransition al método build() para ChatMessage:

  1. En el método build() para ChatMessage, selecciona la primera instancia Container.
  2. Presiona Option+Return (macOS) o Alt+Enter (Linux y Windows) para que aparezca un menú y selecciona Wrap with widget.
  3. Ingresa SizeTransition. Aparecerá un cuadro rojo alrededor de la propiedad child:. Esto indica que falta una propiedad obligatoria en la clase del widget. Coloca el cursor sobre SizeTransition,, la información sobre la herramienta indica que se requiere sizeFactor y ofrece crearlo. Selecciona esa opción, y la propiedad aparecerá con un valor null.
  4. Reemplaza null por una instancia CurvedAnimation. Esto agrega el código estándar para dos propiedades: parent (obligatorio) y curve.
  5. En la propiedad parent, reemplaza null por animationController.
  6. Para la propiedad curve, reemplaza null por Curves.easeOut, una de las constantes de la clase Curves.
  7. Agrega una línea después de sizeFactor (pero en el mismo nivel) y, luego, ingresa una propiedad axisAlignment para SizeTransition, con un valor de 0.0.
@override
Widget build(BuildContext context) {
  return SizeTransition(             // NEW
    sizeFactor:                      // NEW
        CurvedAnimation(parent: animationController, curve: Curves.easeOut),  // NEW
    axisAlignment: 0.0,              // NEW
    child: Container(                // MODIFIED
    ...

cf1e10b838bf60ee.png Observaciones

  • El objeto CurvedAnimation, junto con la clase SizeTransition, produce un efecto de animación de salida lenta. El efecto de salida lenta hace que el mensaje se deslice rápidamente hacia arriba al comienzo de la animación y se reduzca la velocidad hasta su detención.
  • El widget SizeTransition se comporta como un elemento ClipRect animado de animación que expone cada vez más el texto a medida que se desliza.

Eliminación de la animación

Se recomienda eliminar los controladores de la animación para liberar tus recursos cuando ya no son necesarios.

b2f84ff91b0e1396.pngAgrega el método dispose() a _ChatScreenState.

Agrega el siguiente método a la parte inferior de _ChatScreenState:

@override
void dispose() {
  for (var message in _messages){
    message.animationController.dispose();
  }
  super.dispose();
}

b2f84ff91b0e1396.pngAhora, el código es correcto, pero no tiene el formato adecuado. Haz clic con el botón derecho en el panel de código y selecciona Reformat Code with dartfmt.

b2f84ff91b0e1396.pngRealiza la recarga en caliente de la app (o el reinicio en caliente si la app contiene mensajes de chat) y luego ingresa algunos mensajes para observar el efecto de animación.

Si quieres seguir experimentando con las animaciones, estas son algunas ideas que puedes probar:

  • Para acelerar o ralentizar la animación, modifica el valor duration especificado en el método _handleSubmitted().
  • Especifica diferentes curvas de animación mediante las constantes definidas en la clase Curves.
  • Para crear un efecto de animación que se atenúa, ajusta el elemento Container en un widget FadeTransition en lugar de un elemento SizeTransition.

¿Tienes problemas?

Si tu app no se ejecuta correctamente, comprueba que no haya errores ortográficos. Si es necesario, usa el código que aparece en el siguiente vínculo para volver a empezar.

En este paso opcional, tu app recibe algunos detalles sofisticados, como habilitar el botón Enviar solo cuando hay texto para enviar, unir mensajes más largos y agregar personalizaciones de estilo nativo para Android y iOS.

Haz que el botón de enviar sea contextual

Actualmente, el botón Enviar aparece habilitado, incluso cuando no hay texto en el campo de entrada. Recomendamos que la apariencia del botón cambie si el campo contiene texto para enviar.

b2f84ff91b0e1396.pngDefine _isComposing, una variable privada que es verdadera cuando el usuario escribe en el campo de entrada:

class _ChatScreenState extends State<ChatScreen> with TickerProviderStateMixin {
  final List<ChatMessage> _messages = [];
  final _textController = TextEditingController();
  final FocusNode _focusNode = FocusNode();
  bool _isComposing = false;            // NEW

b2f84ff91b0e1396.pngAgrega un método de devolución de llamada onChanged() a _ChatScreenState.

En el método _buildTextComposer(), agrega la propiedad onChanged a TextField y actualiza la propiedad onSubmitted:

Flexible(
  child: TextField(
    controller: _textController,
    onChanged: (String text) {            // NEW
      setState(() {                       // NEW
        _isComposing = text.isNotEmpty;   // NEW
      });                                 // NEW
    },                                    // NEW
    onSubmitted: _isComposing ? _handleSubmitted : null, // MODIFIED
    decoration:
        InputDecoration.collapsed(hintText: 'Send a message'),
    focusNode: _focusNode,
  ),
),

b2f84ff91b0e1396.pngActualiza el método de devolución de llamada onPressed() en _ChatScreenState.

En el método _buildTextComposer(), actualiza la propiedad onPressed para IconButton:

Container(
  margin: EdgeInsets.symmetric(horizontal: 4.0),
  child: IconButton(
      icon: const Icon(Icons.send),
      onPressed: _isComposing                            // MODIFIED
          ? () => _handleSubmitted(_textController.text) // MODIFIED
          : null,                                        // MODIFIED
      )
      ...
)

b2f84ff91b0e1396.pngModifica _handleSubmitted para configurar _isComposing como falso cuando el campo de texto se borre:

void _handleSubmitted(String text) {
  _textController.clear();
  setState(() {                             // NEW
    _isComposing = false;                   // NEW
  });                                       // NEW

  ChatMessage message = ChatMessage(
  ...

cf1e10b838bf60ee.png Observaciones

  • La devolución de llamada onChanged notifica a TextField que el usuario editó su texto. TextField llama a este método siempre que cambia su valor del valor actual del campo.
  • La devolución de llamada onChanged llama a setState() para cambiar el valor de _isComposing a verdadero cuando el campo contiene texto.
  • Cuando _isComposing es falso, la propiedad onPressed se configura como null.
  • La propiedad onSubmitted también se modificó para que no agregue una string vacía a la lista de mensajes.
  • La variable _isComposing ahora controla el comportamiento y el aspecto visual del botón Enviar.
  • Si el usuario escribe una string en el campo de texto, _isComposing es true, y el color del botón se establece en Theme.of(context).accentColor. Cuando el usuario presiona el botón Enviar, el marco de trabajo invoca a _handleSubmitted().
  • Si el usuario no escribe nada en el campo de texto, entonces _isComposing esfalse, y la propiedad onPressed del widget se establece ennull, lo que inhabilita Enviar. El marco de trabajo cambia automáticamente el color del botón a Theme.of(context).disabledColor.

b2f84ff91b0e1396.pngCarga nuevamente la app para probarla.

Cómo unir líneas largas

Cuando un usuario envía un mensaje de chat que excede el ancho de la IU para mostrar mensajes, las líneas deben unirse para que se muestre todo el mensaje. En este momento, se truncan las líneas que se desbordan y aparece un error visual de desbordamiento. Una forma sencilla de asegurarte de que el texto se ajuste de forma correcta es colocarlo dentro de un widget Expanded.

b2f84ff91b0e1396.pngUne el widget Column con un widget Expanded:

  1. En el método build() para ChatMessage, selecciona el widget Column dentro de Row para Container.
  2. Presiona Option+Return (macOS) o Alt+Enter (Linux y Windows) para que aparezca el menú.
  3. Comienza a escribir Expanded, y selecciona Expanded en la lista de objetos posibles.

En la siguiente muestra de código, se muestra cómo se ve la clase ChatMessage después de realizar este cambio:

...
Container(
  margin: const EdgeInsets.only(right: 16.0),
  child: CircleAvatar(child: Text(_name[0])),
),
Expanded(            // NEW
  child: Column(     // MODIFIED
    crossAxisAlignment: CrossAxisAlignment.start,
    children: [
      Text(_name, style: Theme.of(context).textTheme.headline4),
      Container(
        margin: EdgeInsets.only(top: 5.0),
        child: Text(text),
      ),
    ],
  ),
),                    // NEW
...

cf1e10b838bf60ee.png Observaciones

El widget de Expanded permite que su widget secundario (como Column) imponga las restricciones de diseño (en este caso, el ancho de Column) en un widget secundario. Aquí, restringe el ancho del widget Text, que normalmente se determina por su contenido.

Personalización para iOS y Android

Para que la IU de tu app tenga un aspecto natural, puedes agregar un tema y una lógica simple al método build() para la clase FriendlyChatApp. En este paso, se define un tema de plataforma que aplica un conjunto diferente de colores primarios y destacados. También puedes personalizar el botón Enviar para usar un IconButton de Material Design en Android y un CupertinoButton en iOS.

b2f84ff91b0e1396.pngAgrega el siguiente código a main.dart, después del método main():

final ThemeData kIOSTheme = ThemeData(
  primarySwatch: Colors.orange,
  primaryColor: Colors.grey[100],
  primaryColorBrightness: Brightness.light,
);

final ThemeData kDefaultTheme = ThemeData(
  primarySwatch: Colors.purple,
  accentColor: Colors.orangeAccent[400],
);

cf1e10b838bf60ee.png Observaciones

  • El objeto kDefaultTheme ThemeData especifica colores para Android (en violeta con toques naranjas).
  • El objeto kIOSTheme ThemeData especifica los colores para iOS (gris claro con detalles en naranja).

b2f84ff91b0e1396.pngModifica la clase FriendlyChatApp para variar el tema por medio de la propiedad theme del widget MaterialApp de tu app:

  1. Importa el paquete de base en la parte superior del archivo:
import 'package:flutter/foundation.dart';  // NEW
import 'package:flutter/material.dart';
  1. Modifica la clase FriendlyChatApp para elegir un tema apropiado:
class FriendlyChatApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'FriendlyChat',
      theme: defaultTargetPlatform == TargetPlatform.iOS // NEW
        ? kIOSTheme                                      // NEW
        : kDefaultTheme,                                 // NEW
      home: ChatScreen(),
    );
  }
}

b2f84ff91b0e1396.pngModifica el tema del widget AppBar (el banner que aparece en la parte superior de la IU de tu app).

  1. En el método build() de _ChatScreenState, busca la siguiente línea de código:
      appBar: AppBar(title: Text('FriendlyChat')),
  1. Coloca el cursor entre los dos paréntesis de cierre ())), escribe una coma y presiona Intro para iniciar una línea nueva.
  2. Agrega las siguientes dos líneas:
elevation:
   Theme.of(context).platform == TargetPlatform.iOS ? 0.0 : 4.0, // NEW
  1. Haz clic con el botón derecho en el panel de código y selecciona Reformat code with dartfmt.

cf1e10b838bf60ee.png Observaciones

  • La propiedad defaultTargetPlatform y los operadores condicionales de nivel superior se usan para seleccionar el tema.
  • La propiedad elevation define las coordenadas z de AppBar. Un valor de coordenadas z de 4.0 tiene una sombra definida (Android) y un valor de 0.0 no tiene ninguna sombra (iOS). .

b2f84ff91b0e1396.pngPersonaliza el ícono de envío para Android y iOS.

  1. Agrega la siguiente importación a la parte superior de main.dart:
import 'package:flutter/cupertino.dart';   // NEW
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
  1. En el método _buildTextComposer() de _ChatScreenState, modifica la línea que asigna un IconButton como elemento secundario de Container. Cambia la asignación para que sea condicional en la plataforma. Para iOS, usa un elemento CupertinoButton. De lo contrario, permanece con un IconButton:
Container(
   margin: EdgeInsets.symmetric(horizontal: 4.0),
   child: Theme.of(context).platform == TargetPlatform.iOS ? // MODIFIED
   CupertinoButton(                                          // NEW
     child: Text('Send'),                                    // NEW
     onPressed: _isComposing                                 // NEW
         ? () =>  _handleSubmitted(_textController.text)     // NEW
         : null,) :                                          // NEW
   IconButton(                                               // MODIFIED
       icon: const Icon(Icons.send),
       onPressed: _isComposing ?
           () =>  _handleSubmitted(_textController.text) : null,
       )
   ),

b2f84ff91b0e1396.pngUne el Column de nivel superior en un widget Container y asígnale un borde gris claro en su borde superior.

Este borde ayuda a distinguir visualmente la barra de la aplicación del cuerpo de la app en iOS. Para ocultar el borde en Android, aplica la misma lógica que se usa en la barra de la aplicación de la muestra de código anterior:

  1. En el método build() de _ChatScreenState, selecciona el elemento Column que aparece después de body:.
  2. Presiona Option+Return (macOS) o Alt+Enter (Linux y Windows) para que aparezca un menú y selecciona Wrap with Container.
  3. Después del final del elemento Column, pero antes de que finalice Container, agrega el código (que se muestra) que agrega condicionalmente el botón adecuado según la plataforma.
@override
Widget build(BuildContext context) {
  return Scaffold(
    appBar: AppBar(
      title: Text('FriendlyChat'),
      elevation:
          Theme.of(context).platform == TargetPlatform.iOS ? 0.0 : 4.0, // NEW
    ),
    body: Container(
        child: Column(
          children: [
            Flexible(
              child: ListView.builder(
                padding: EdgeInsets.all(8.0),
                reverse: true,
                itemBuilder: (_, int index) => _messages[index],
                itemCount: _messages.length,
              ),
            ),
            Divider(height: 1.0),
            Container(
              decoration: BoxDecoration(color: Theme.of(context).cardColor),
              child: _buildTextComposer(),
            ),
          ],
        ),
        decoration: Theme.of(context).platform == TargetPlatform.iOS // NEW
            ? BoxDecoration(                                 // NEW
                border: Border(                              // NEW
                  top: BorderSide(color: Colors.grey[200]!), // NEW
                ),                                           // NEW
              )                                              // NEW
            : null),                                         // MODIFIED
  );
}

b2f84ff91b0e1396.pngVuelve a cargar la app en caliente. Deberías ver distintos colores, sombras y botones de íconos para Android y iOS.

Pixel 3 XL

iPhone 11

¿Tienes problemas?

Si tu app no se ejecuta correctamente, comprueba que no haya errores ortográficos. Si es necesario, usa el código que aparece en el siguiente vínculo para volver a empezar.

¡Felicitaciones!

Ahora, conoces los conceptos básicos de la compilación de apps para dispositivos móviles multiplataforma con el marco de trabajo de Flutter.

Temas abordados

  • Cómo crear una app de Flutter desde cero
  • Cómo usar algunos de los accesos directos proporcionados en IntelliJ y Android Studio
  • Cómo ejecutar, recargar en caliente y depurar tu app de Flutter en un emulador, un simulador y un dispositivo
  • Cómo personalizar tu interfaz de usuario con widgets y animaciones
  • Cómo personalizar tu interfaz de usuario para iOS y Android

¿Qué sigue?

Prueba uno de los demás codelab de Flutter.

Sigue aprendiendo sobre Flutter:

Para obtener más información sobre las combinaciones de teclas, consulta lo siguiente:

Puedes descargar la muestra de código para ver los ejemplos como referencia o iniciar el codelab en una sección específica. Para obtener una copia del código de muestra del codelab, ejecuta este comando desde tu terminal:

 git clone https://github.com/flutter/codelabs

El código de muestra de este codelab se encuentra en la carpeta friendly_chat. Cada carpeta de pasos numeradas se alinea con la apariencia del código al final de los pasos numerados de este codelab. También puedes soltar el código del archivo lib/main.dart desde cualquiera de estos pasos en una instancia de ArtPad y ejecutarlos desde allí.