Tu primera app de Flutter

1. Introducción

Flutter es el kit de herramientas de IU de Google diseñado para crear aplicaciones que funcionen en dispositivos móviles, la Web y computadoras de escritorio a partir de una base de código única. En este codelab, compilarás la siguiente aplicación de Flutter:

1d26af443561f39c.gif

La aplicación genera nombres que suenan bien, como "newstay", "lightstream", "mainbrake" o "graypine". El usuario puede solicitar otro nombre, marcar como favorito el actual y revisar la lista de nombres favoritos en una página independiente. La app es responsiva y se adapta a distintos tamaños de pantalla.

Qué aprenderás

  • Cuáles son los conceptos básicos del funcionamiento de Flutter
  • Cómo crear diseños en Flutter
  • Cómo conectar las interacciones del usuario (como la presión de un botón) con el comportamiento de la app
  • Cómo mantener organizado tu código de Flutter
  • Cómo hacer que tu app sea responsiva (en distintas pantallas)
  • Cómo lograr que tu app tenga un aspecto y una experiencia coherentes

Comenzarás con un andamiaje básico de modo que puedas pasar directamente a las partes interesantes.

d6e3d5f736411f13.png

A continuación, Filip te guiará por todo el codelab.

Haz clic en next para comenzar el lab.

2. Configura tu entorno de Flutter

Editor

Para hacer este codelab lo más simple posible, se asumirá que usas Visual Studio Code (VS Code) como tu entorno de desarrollo. Es gratuito y funciona en las principales plataformas.

Por supuesto, no hay problema si usas cualquier otro editor de tu preferencia: Android Studio, otros IDE de IntelliJ, Emacs, Vim o Notepad++. Todos funcionan con Flutter.

Te recomendamos que uses VS Code para este codelab porque las instrucciones predeterminadas indican combinaciones de teclas específicas de VS Code. Es más fácil decir algo así como "haz clic aquí" o "presiona esta tecla" en lugar de decir algo como "realiza la acción apropiada en tu editor para hacer X".

15961a28a4500ac1.png

Elige un segmento de desarrollo

Flutter es un kit de herramientas multiplataforma. Tu app puede ejecutarse en cualquiera de los siguientes sistemas operativos:

  • iOS
  • Android
  • Windows
  • macOS
  • Linux
  • Web

Sin embargo, es una práctica común elegir un único sistema operativo para tu desarrollo primario. Este será tu "segmento de desarrollo": el sistema operativo que tu app ejecutará durante el desarrollo.

d105428cb3aae7d5.png

Por ejemplo, digamos que usas una laptop con Windows para desarrollar una app de Flutter. Si eliges Android como tu segmento de desarrollo, deberás conectar un dispositivo Android a tu laptop mediante un cable USB, y tu app en desarrollo se ejecutará en el dispositivo conectado. Pero también puedes optar por Windows como segmento de desarrollo, lo que significa que tu app en desarrollo se ejecutará como una app de Windows junto a tu editor.

Puede ser tentador elegir la Web como el segmento de desarrollo. La desventaja de esta elección es que perderás una de las funciones de desarrollo más útiles que tiene Flutter: la recarga en caliente con estado. Flutter no puede hacer recargas en caliente de aplicaciones web.

Elige ahora. Recuerda que podrás ejecutar tu app en otros sistemas operativos más adelante. Tener en la mente un segmento de desarrollo claro simplifica los próximos pasos.

Instala Flutter

Podrás encontrar las instrucciones más actualizadas para instalar el SDK de Flutter en docs.flutter.dev.

Las instrucciones del sitio web de Flutter abarcan la instalación del SDK y también los complementos y las herramientas relacionadas con el segmento de desarrollo. Recuerda que, para este codelab, solo necesitas instalar lo siguiente:

  1. El SDK de Flutter
  2. Visual Studio Code con el complemento de Flutter
  3. El software que requiera tu segmento de desarrollo (por ejemplo, Visual Studio para segmentar a Windows o Xcode para segmentar a macOS)

En la siguiente sección, crearás tu primer proyecto de Flutter.

Si tienes problemas, consulta estas preguntas y respuestas (de StackOverflow), que te resultarán útiles para solucionarlos.

Preguntas frecuentes

3. Crea un proyecto

Crea tu primer proyecto de Flutter

Inicia Visual Studio Code y abre la paleta de comandos (con F1, Ctrl+Shift+P o Shift+Cmd+P). Comienza a ingresar escribir "flutter new". Selecciona el comando Flutter: New Project.

58e8487afebfc1dd.gif

A continuación, selecciona Application y, luego, una carpeta en la que se creará tu proyecto. Podría ser tu directorio principal o alguno como C:\src\.

Por último, asígnale un nombre al proyecto. Uno como namer_app o my_awesome_namer.

260a7d97f9678005.png

Flutter ahora creará la carpeta del proyecto y VS Code lo abrirá.

Ahora reemplazarás el contenido de 3 archivos con un andamiaje básico de la app.

Copia y pega la app inicial

En el panel izquierdo de VS Code, asegúrate de que se haya seleccionado Explorer y abre el archivo pubspec.yaml.

e2a5bab0be07f4f7.png

Reemplaza el contenido de este archivo con lo siguiente:

pubspec.yaml

name: namer_app
description: A new Flutter project.

publish_to: 'none' # Remove this line if you wish to publish to pub.dev

version: 0.0.1+1

environment:
  sdk: '>=2.19.4 <4.0.0'

dependencies:
  flutter:
    sdk: flutter

  english_words: ^4.0.0
  provider: ^6.0.0

dev_dependencies:
  flutter_test:
    sdk: flutter

  flutter_lints: ^2.0.0

flutter:
  uses-material-design: true

El archivo pubspec.yaml especifica la información básica de tu app, como la versión actual, las dependencias y los recursos con los que se enviará.

A continuación, abre otro archivo de configuración del proyecto, analysis_options.yaml.

a781f218093be8e0.png

Reemplaza su contenido con lo siguiente:

analysis_options.yaml

include: package:flutter_lints/flutter.yaml

linter:
  rules:
    prefer_const_constructors: false
    prefer_final_fields: false
    use_key_in_widget_constructors: false
    prefer_const_literals_to_create_immutables: false
    prefer_const_constructors_in_immutables: false
    avoid_print: false

En este archivo, se determina cuán estricto debe ser Flutter a la hora de analizar tu código. Dado que esta es tu primera incursión en Flutter, le dirás al analizador que se lo tome con calma. Podrás ajustar esto más adelante. De hecho, a medida que te acerques al momento de publicar una app de producción real, seguramente querrás que el analizador sea más estricto que esto.

Por último, abre el archivo main.dart que se encuentra en el directorio lib/.

e54c671c9bb4d23d.png

Reemplaza el contenido de este archivo con lo siguiente:

lib/main.dart

import 'package:english_words/english_words.dart';
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';

void main() {
  runApp(MyApp());
}

class MyApp extends StatelessWidget {
  const MyApp({super.key});

  @override
  Widget build(BuildContext context) {
    return ChangeNotifierProvider(
      create: (context) => MyAppState(),
      child: MaterialApp(
        title: 'Namer App',
        theme: ThemeData(
          useMaterial3: true,
          colorScheme: ColorScheme.fromSeed(seedColor: Colors.deepOrange),
        ),
        home: MyHomePage(),
      ),
    );
  }
}

class MyAppState extends ChangeNotifier {
  var current = WordPair.random();
}

class MyHomePage extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    var appState = context.watch<MyAppState>();

    return Scaffold(
      body: Column(
        children: [
          Text('A random idea:'),
          Text(appState.current.asLowerCase),
        ],
      ),
    );
  }
}

Estas 50 líneas de código son la totalidad de tu app hasta el momento.

En la próxima sección, ejecutarás la aplicación en el modo de depuración y comenzarás a desarrollar.

4. Agrega un botón

En este paso, se agrega un botón Next para generar una nueva vinculación de palabras.

Inicia la app

Primero, abre lib/main.dart y asegúrate de que hayas seleccionado el dispositivo de destino. En el extremo inferior derecho de VS Code, encontrarás un botón que muestra el dispositivo actual. Haz clic para cambiarlo.

6c4474b4b5e92ffb.gif

Mientras lib/main.dart está abierto, busca el botón de "reproducir" b0a5d0200af5985d.png en el extremo superior derecho de la ventana de VS Code y haz clic en él.

9b7598a38a6412e6.gif

Aproximadamente un minuto después, se iniciará tu app en modo de depuración. No parece gran cosa todavía:

f96e7dfb0937d7f4.png

Primera recarga en caliente

En la parte inferior de lib/main.dart, agrega algo a la cadena del primer objeto Text y guarda el archivo (con Ctrl+S o Cmd+S). Por ejemplo:

lib/main.dart

// ...

    return Scaffold(
      body: Column(
        children: [
          Text('A random AWESOME idea:'),  // ← Example change.
          Text(appState.current.asLowerCase),
        ],
      ),
    );

// ...

Observa cómo cambia la app inmediatamente, pero la palabra aleatoria sigue siendo la misma. Esta es la famosa recarga en caliente con estado de Flutter. La recarga en caliente se activa cuando guardas cambios en un archivo fuente.

1b05b00515b3ecec.gif

Preguntas frecuentes

Cómo agregar un botón

A continuación, agrega un botón en la parte inferior de Column, justo debajo de la segunda instancia de Text.

lib/main.dart

// ...

    return Scaffold(
      body: Column(
        children: [
          Text('A random AWESOME idea:'),
          Text(appState.current.asLowerCase),

          // ↓ Add this.
          ElevatedButton(
            onPressed: () {
              print('button pressed!');
            },
            child: Text('Next'),
          ),

        ],
      ),
    );

// ...

Cuando guardes el cambio, la app se actualizará otra vez: aparecerá un botón y, cuando hagas clic en él, la Consola de depuración de VS Code mostrará un mensaje de button pressed!, que indica que se presionó un botón.

8d86426a01e28011.gif

Un curso rápido de Flutter en 5 minutos

Aunque resulte divertido mirar la Consola de depuración, querrás que el botón haga algo más útil. Antes de abordar eso, observa atentamente el código de lib/main.dart para comprender su funcionamiento.

lib/main.dart

// ...

void main() {
  runApp(MyApp());
}

// ...

En la parte superior del archivo, encontrarás la función main(). En su forma actual, solo le indica a Flutter que ejecute la app definida en MyApp.

lib/main.dart

// ...

class MyApp extends StatelessWidget {
  const MyApp({super.key});

  @override
  Widget build(BuildContext context) {
    return ChangeNotifierProvider(
      create: (context) => MyAppState(),
      child: MaterialApp(
        title: 'Namer App',
        theme: ThemeData(
          useMaterial3: true,
          colorScheme: ColorScheme.fromSeed(seedColor: Colors.deepOrange),
        ),
        home: MyHomePage(),
      ),
    );
  }
}

// ...

La clase MyApp extiende StatelessWidget. Los widgets son los elementos a partir de los cuales compilarás cada app de Flutter. Como puedes ver, incluso la propia app es un widget.

El código de MyApp configura la app por completo. Crea un estado de toda la app (hablaremos de esto más adelante), le asigna un nombre a la app, define el tema visual y establece el widget "principal" (el punto de partida de tu app).

lib/main.dart

// ...

class MyAppState extends ChangeNotifier {
  var current = WordPair.random();
}

// ...

A continuación, la clase MyAppState define el estado de la app. Esta es tu primera incursión en Flutter, por lo que en este codelab seguiremos un criterio simple y enfocado. Hay muchas maneras poderosas de gestionar el estado de la app en Flutter. Una de las más fáciles de explicar es ChangeNotifier, el enfoque que utiliza esta app.

  • MyAppState define los datos que la app necesita para funcionar. En este momento, solo contiene una única variable con el par actual de palabras aleatorias. Cambiarás esto más adelante.
  • La clase de estado extiende ChangeNotifier, lo que significa que puede notificar a otros acerca de sus propios cambios. Por ejemplo, si el par actual de palabras cambia, algunos widgets de la app deben saber esto.
  • El estado se crea y se brinda a toda la app mediante un ChangeNotifierProvider (consulta el código anterior en MyApp). Esto le permite a cualquier widget de la app obtener el estado. d9b6ecac5494a6ff.png

lib/main.dart

// ...

class MyHomePage extends StatelessWidget {
  @override
  Widget build(BuildContext context) {           // ← 1
    var appState = context.watch<MyAppState>();  // ← 2

    return Scaffold(                             // ← 3
      body: Column(                              // ← 4
        children: [
          Text('A random AWESOME idea:'),        // ← 5
          Text(appState.current.asLowerCase),    // ← 6
          ElevatedButton(
            onPressed: () {
              print('button pressed!');
            },
            child: Text('Next'),
          ),
        ],                                       // ← 7
      ),
    );
  }
}

// ...

Por último, tenemos el elemento MyHomePage, el widget que ya modificaste. Cada línea numerada debajo está asignada a un comentario de número de línea en el código anterior:

  1. Cada widget define un método build() que se llama automáticamente cada vez que cambian las circunstancias del widget de modo que este siempre esté actualizado.
  2. MyHomePage realiza el seguimiento del estado actual de la app usando el método watch.
  3. Cada método build debe mostrar un widget o un árbol de widgets anidado (algo más típico). En este caso, el widget de nivel superior es Scaffold. No vas a trabajar con Scaffold en este codelab, pero es un widget útil y se encuentra en la gran mayoría de las apps de Flutter del mundo real.
  4. Column es uno de los widgets de diseño más básicos de Flutter. Toma una cantidad cualquiera de elementos secundarios y los encolumna desde arriba hacia abajo. De forma predeterminada, la columna ubica visualmente sus elementos secundarios en la parte superior. Pronto cambiarás esto de modo que la columna esté alineada en el centro.
  5. Cambiaste este widget de Text en el primer paso.
  6. Este segundo widget de Text toma el appState y accede al único miembro de esa clase, current (que es un WordPair). WordPair proporciona varios métodos get de utilidad, como asPascalCase o asSnakeCase. Aquí, usaremos asLowerCase, pero puedes cambiar esto ahora si prefieres alguna otra alternativa.
  7. Observa cómo el código de Flutter usa en gran medida las comas finales. Esta coma en particular no necesita estar aquí, ya que children es el último (y el único) miembro de esta particular lista de parámetros de Column. Sin embargo, en general, resulta útil usar las comas finales: hace que agregar más miembros sea algo trivial y sirve como una pista para que el aplicador de formato automático de Dart sepa que debe insertar una nueva línea ahí. Si deseas obtener más información, consulta Formato del código.

A continuación, conectarás el botón al estado.

Tu primer comportamiento

Desplázate hasta MyAppState y agrega un método getNext.

lib/main.dart

// ...

class MyAppState extends ChangeNotifier {
  var current = WordPair.random();

  // ↓ Add this.
  void getNext() {
    current = WordPair.random();
    notifyListeners();
  }
}

// ...

El nuevo método getNext() reasignará el elemento current con un nuevo WordPair aleatorio. También llamará a notifyListeners() (un método de ChangeNotifier) que garantiza que se notifique a todo elemento que esté mirando a MyAppState.

Lo que resta es llamar al método getNext desde la devolución de llamada del botón.

lib/main.dart

// ...

    ElevatedButton(
      onPressed: () {
        appState.getNext();  // ← This instead of print().
      },
      child: Text('Next'),
    ),

// ...

Ahora guarda y ejecuta la app. Debería generar un nuevo par de palabras cada vez que presiones el botón Next.

En la siguiente sección, mejorarás la estética de la interfaz de usuario.

5. Haz que la app sea más atractiva

Así es como se ve la app en este momento.

3dd8a9d8653bdc56.png

No se ve muy bien. La parte central de la app, el par de palabras generado aleatoriamente, debería ser más visible. Después de todo, es la razón principal por la que los usuarios están usando esta app. Además, el contenido de la app aparece descentrado de forma extraña, y la app luce aburrida con sus colores blanco y negro.

En esta sección, trabajaremos en el diseño de la app para abordar estas cuestiones. El objetivo final es lograr algo como lo siguiente:

2bbee054d81a3127.png

Extrae un widget

Por ahora, la línea responsable de mostrar el par actual de palabras tiene el siguiente aspecto: Text(appState.current.asLowerCase). Para cambiarlo por algo más complejo, es una buena idea extraer esta línea en un widget independiente. Tener distintos widgets para partes lógicas e independientes de tu IU es una manera importante de administrar la complejidad en Flutter.

Flutter ofrece un método auxiliar de refactorización que extrae widgets; pero, antes de usarlo, asegúrate de que la línea que estás extrayendo acceda solo a lo que necesite. En este momento, la línea accede a appState, pero solo necesita conocer el par actual de palabras.

Por ese motivo, vuelve a escribir el widget de MyHomePage como se indica a continuación:

lib/main.dart

// ...

class MyHomePage extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    var appState = context.watch<MyAppState>();
    var pair = appState.current;                 // ← Add this.

    return Scaffold(
      body: Column(
        children: [
          Text('A random AWESOME idea:'),
          Text(pair.asLowerCase),                // ← Change to this.
          ElevatedButton(
            onPressed: () {
              appState.getNext();
            },
            child: Text('Next'),
          ),
        ],
      ),
    );
  }
}

// ...

Muy bien. El widget de Text ya no hace referencia al appState completo.

Ahora, despliega el menú Refactor. En VS Code, puedes hacer esto de dos maneras:

  1. Haz clic con el botón derecho en la parte del código que quieres refactorizar (en este caso, Text) y selecciona Refactor… en el menú desplegable.

O

  1. Mueve el cursor hacia la parte del código que quieres refactorizar (en este caso, Text) y presiona Ctrl+. (Windows/Linux) o Cmd+. (Mac).

9e18590d82a6900.gif

En el menú Refactor, selecciona Extract Widget. Asigna un nombre, como BigCard, y haz clic en Enter.

Esto creará automáticamente una clase nueva, BigCard, al final del archivo actual. La clase tendrá un aspecto similar al siguiente:

lib/main.dart

// ...

class BigCard extends StatelessWidget {
  const BigCard({
    super.key,
    required this.pair,
  });

  final WordPair pair;

  @override
  Widget build(BuildContext context) {
    return Text(pair.asLowerCase);
  }
}

// ...

Observa que la app sigue funcionando incluso durante esta refactorización.

Agrega una tarjeta

Ahora es momento de colocar este widget nuevo en la parte llamativa de la IU que imaginamos al comienzo de esta sección.

Busca la clase BigCard y el método build() dentro de ella. Como antes, despliega el menú Refactor en el widget de Text. Sin embargo, esta vez no vas a extraer el widget.

En su lugar, selecciona Wrap with Padding. Esto creará un nuevo widget superior alrededor del widget de Text llamado Padding. Luego de guardar, verás que la palabra aleatoria ya tiene más espacio a su alrededor.

6b585b43e4037c65.gif

Aumenta el valor predeterminado del padding, que es 8.0. Por ejemplo, usa algo como 20 para lograr un padding más espacioso.

A continuación, avancemos a un nivel superior. Coloca el cursor en el widget de Padding, despliega el menú Refactor y selecciona Wrap with widget…

Esto te permitirá especificar el widget superior. Ingresa "Card" y presiona Intro.

523425642904374.gif

Esto une el widget de Padding, y, por lo tanto, también el Text, con un widget de Card.

6031adbc0a11e16b.png

Tema y estilo

Para lograr que la tarjeta se destaque más, píntala con un color más intenso. Y, dado que siempre es una buena idea mantener un esquema de colores coherente, usa el Theme de la app para elegir el color.

Haz los siguientes cambios en el método build() de BigCard.

lib/main.dart

// ...

  @override
  Widget build(BuildContext context) {
    final theme = Theme.of(context);       // ← Add this.

    return Card(
      color: theme.colorScheme.primary,    // ← And also this.
      child: Padding(
        padding: const EdgeInsets.all(20),
        child: Text(pair.asLowerCase),
      ),
    );
  }

// ...

Estas dos líneas realizan varias tareas:

  • Primero, el código solicita el tema actual de la app con Theme.of(context).
  • Luego, el código define el color de la tarjeta de modo que sea el mismo que el de la propiedad colorScheme del tema. El esquema de colores contiene varios de ellos, y primary es el más destacado y el que define el color de la app.

Ahora la tarjeta está pintada del color primario de la app:

a136f7682c204ea1.png

Puedes cambiar este color, y el esquema de colores de la app completa, si te desplazas hacia arriba hasta MyApp y cambias el color de origen del ColorScheme que figura allí.

5bd5a50b5d08f5fb.gif

Observa cómo se anima el color sin problemas. Esto se conoce como animación implícita. Muchos widgets de Flutter harán una interpolación fluida entre valores de modo que la IU no "salte" de un estado a otro.

El botón elevado que se indica debajo de la tarjeta también cambiará de color. Esa es la gran ventaja de usar un Theme en toda la app en lugar de usar valores hard-coded.

TextTheme

La tarjeta aún tiene un problema: el texto es demasiado pequeño y su color dificulta la lectura. Para arreglar esto, haz los siguientes cambios en el método build() de BigCard.

lib/main.dart

// ...

  @override
  Widget build(BuildContext context) {
    final theme = Theme.of(context);
    // ↓ Add this.
    final style = theme.textTheme.displayMedium!.copyWith(
      color: theme.colorScheme.onPrimary,
    );

    return Card(
      color: theme.colorScheme.primary,
      child: Padding(
        padding: const EdgeInsets.all(20),
        // ↓ Change this line.
        child: Text(pair.asLowerCase, style: style),
      ),
    );
  }

// ...

Qué tenemos detrás de este cambio:

  • Cuando usas theme.textTheme,, accedes al tema de la fuente de tu app. Esta clase incluye miembros como bodyMedium (para texto estándar de tamaño mediano), caption (para leyendas de imágenes) o headlineLarge (para titulares grandes).
  • La propiedad displayMedium tiene un estilo grande diseñado para texto de visualización. La palabra visualización se usa aquí en un sentido tipográfico, como cuando se habla de tipo de letra de visualización. En la documentación de displayMedium, se indica que "los estilos de visualización se reservan para texto corto e importante", exactamente nuestro caso de uso.
  • La propiedad displayMedium del tema, en teoría, podría ser null. Dart, el lenguaje de programación en el que estás escribiendo esta app, tiene seguridad contra nulos, de modo que no te permitirá llamar a métodos de objetos que podrían llegar a ser null. En este caso, sin embargo, puedes usar el operador ! ("operador bang") para asegurarle a Dart que sabes lo que haces. Definitivamente, displayMedium no es nulo en este caso; pero el motivo por el que sabemos esto está más allá del alcance de este codelab.
  • Llamar a copyWith() en displayMedium muestra una copia del estilo del texto con los cambios que definas. En este caso, solamente estás cambiando el color del texto.
  • Para obtener el color nuevo, accede una vez más al tema de la app. La propiedad onPrimary del esquema de colores define un color que resulta una buena opción para usar en el color primario de la app.

La app debería tener un aspecto similar al siguiente:

2405e9342d28c193.png

Si quieres, puedes hacer más cambios en la tarjeta. Aquí encontrarás algunas ideas:

  • copyWith() te permite cambiar mucho más del estilo de texto que su color. Para obtener la lista completa de propiedades que puedes cambiar, coloca el cursor dentro de los paréntesis de copyWith() y presiona Ctrl+Shift+Space (Windows/Linux) o Cmd+Shift+Space (Mac).
  • De forma similar, puedes hacer más cambios en el widget de Card. Por ejemplo, puedes agrandar la sombra de la tarjeta aumentando el valor del parámetro elevation.
  • Experimenta con los colores. Además de theme.colorScheme.primary, también tienes .secondary, .surface y un montón de colores más. Todos ellos tienen sus equivalentes de onPrimary.

Mejora la accesibilidad

Flutter hace las apps accesibles de forma predeterminada. Por ejemplo, cada app de Flutter muestra correctamente todo el texto y los elementos interactivos de la app en los lectores de pantalla como TalkBack y VoiceOver.

96e3f6d9d36615dd.png

Sin embargo, a veces, es necesario trabajar un poco. En el caso de esta app, el lector de pantalla podría tener problemas a la hora de pronunciar algunos pares generados de palabras. Si bien las personas no tenemos problemas para identificar las dos palabras en inglés en cheaphead, un lector de pantalla podría pronunciar la ph del medio del término como una f.

Una solución simple es reemplazar pair.asLowerCase con "${pair.first} ${pair.second}". Este último usa una interpolación para crear una cadena (como "cheap head") a partir de las dos palabras que contiene pair. Usando dos palabras independientes en lugar de una compuesta, te aseguras de que los lectores de pantalla las identifiquen de forma correcta y brindas una mejor experiencia a los usuarios con discapacidad visual.

Sin embargo, te recomendamos que mantengas la simplicidad visual de pair.asLowerCase. Usa la propiedad semanticsLabel de Text para anular el contenido visual del widget de texto con un contenido semántico que es más apropiado para los lectores de pantalla:

lib/main.dart

// ...

  @override
  Widget build(BuildContext context) {
    final theme = Theme.of(context);
    final style = theme.textTheme.displayMedium!.copyWith(
      color: theme.colorScheme.onPrimary,
    );

    return Card(
      color: theme.colorScheme.primary,
      child: Padding(
        padding: const EdgeInsets.all(20),

        // ↓ Make the following change.
        child: Text(
          pair.asLowerCase,
          style: style,
          semanticsLabel: "${pair.first} ${pair.second}",
        ),
      ),
    );
  }

// ...

Ahora, los lectores de pantalla pronuncian correctamente cada par de palabras generado, pero la IU sigue siendo igual. Observa esto en acción usando un lector de pantalla en tu dispositivo.

Centra la IU

Ahora que el par de palabras aleatorias se presenta con suficiente estilo visual, es hora de colocarlo en el centro de la ventana/pantalla de la app.

Primero, recuerda que BigCard es parte de una Column. De forma predeterminada, las columnas agrupan sus elementos secundarios en la parte superior, pero podemos anular esto con facilidad. Ve al método build() de MyHomePage y realiza el siguiente cambio:

lib/main.dart

// ...

class MyHomePage extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    var appState = context.watch<MyAppState>();
    var pair = appState.current;

    return Scaffold(
      body: Column(
        mainAxisAlignment: MainAxisAlignment.center,  // ← Add this.
        children: [
          Text('A random AWESOME idea:'),
          BigCard(pair: pair),
          ElevatedButton(
            onPressed: () {
              appState.getNext();
            },
            child: Text('Next'),
          ),
        ],
      ),
    );
  }
}

// ...

Esto centrará el elemento secundario dentro de la Column a lo largo de su eje principal (vertical).

b555d4c7f5000edf.png

Los elementos secundarios ya están centrados a lo largo del eje cruzado de la columna (es decir, están centrados horizontalmente). Pero la Column en sí misma no está centrada dentro del Scaffold. Podemos verificar esto usando Widget Inspector.

27c5efd832e40303.gif

Widget Inspector está fuera del alcance de este codelab, pero puedes ver que, cuando la Column está destacada, no ocupa el ancho entero de la app. Solo ocupa tanto espacio horizontal como sus elementos secundarios necesiten.

Puedes centrar la columna. Coloca el cursor sobre Column, despliega el menú Refactor (con Ctrl+. o Cmd+.) y selecciona Wrap with Center.

56418a5f336ac229.gif

La app debería tener un aspecto similar al siguiente:

455688d93c30d154.png

Si lo deseas, puedes hacer algunos ajustes más.

  • Puedes quitar el widget de Text que se encuentra sobre BigCard. Podría decirse que el texto descriptivo ("A random AWESOME idea:") ya no se necesita, ya que la IU tiene sentido sin él. Y se ve más limpia de esa manera.
  • También puedes agregar un widget de SizedBox(height: 10) entre BigCard y ElevatedButton. De esta forma, habrá un poco más de espacio de separación entre los dos widgets. El widget de SizedBox solamente ocupa espacio y no renderiza nada por sí solo. En general, se usa para crear "espacios visuales".

Con los cambios opcionales, MyHomePage contiene este código:

lib/main.dart

// ...

class MyHomePage extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    var appState = context.watch<MyAppState>();
    var pair = appState.current;

    return Scaffold(
      body: Center(
        child: Column(
          mainAxisAlignment: MainAxisAlignment.center,
          children: [
            BigCard(pair: pair),
            SizedBox(height: 10),
            ElevatedButton(
              onPressed: () {
                appState.getNext();
              },
              child: Text('Next'),
            ),
          ],
        ),
      ),
    );
  }
}

// ...

Y la app tiene el siguiente aspecto:

3d53d2b071e2f372.png

En la próxima sección, agregarás la habilidad de marcar como favorito (o como "me gusta") las palabras generadas.

6. Agrega funcionalidad

La app funciona y, en ocasiones, también brinda interesantes pares de palabras. Pero, cuando el usuario hace clic en Next, cada par de palabras desaparece para siempre. Sería mejor tener una forma de "recordar" las mejores sugerencias, como un botón de "me gusta".

e6b01a8c90df8ffa.png

Agrega la lógica empresarial

Desplázate hasta MyAppState y agrega el siguiente código:

lib/main.dart

// ...

class MyAppState extends ChangeNotifier {
  var current = WordPair.random();

  void getNext() {
    current = WordPair.random();
    notifyListeners();
  }

  // ↓ Add the code below.
  var favorites = <WordPair>[];

  void toggleFavorite() {
    if (favorites.contains(current)) {
      favorites.remove(current);
    } else {
      favorites.add(current);
    }
    notifyListeners();
  }
}

// ...

Revisa los cambios:

  • Agregaste una nueva propiedad a MyAppState llamada favorites. Esta propiedad se inicializa con una lista vacía: [].
  • También especificaste que la lista solo puede contener ciertos pares de palabras: <WordPair>[], usando parámetros genéricos. Esto ayudará a que tu app sea más robusta: Dart ni siquiera ejecutará tu app si intentas agregar algo distinto de WordPair a ella. A su vez, puedes usar la lista de favorites a sabiendas de que nunca podrá haber allí objetos no deseados (como null).
  • También agregaste un método, toggleFavorite(), que quita el par actual de palabras de la lista (si ya está en ella) o lo agrega a ella (si aún no está allí). En cualquier caso, el código luego llama a notifyListeners();.

Agrega el botón

Ahora que ya nos ocupamos de la "lógica empresarial", es hora de trabajar sobre la interfaz de usuario una vez más. Ubicar el botón "Like" a la izquierda del botón "Next" requiere una Row. El widget de Row es el equivalente horizontal de Column, que vimos antes.

Primero, une el botón existente en una Row. Ve al método build() de MyHomePage, coloca el cursor en el ElevatedButton, despliega el menú Refactor con Ctrl+. o Cmd+., y selecciona Wrap with Row.

7b9d0ea29e584308.gif

Cuando guardes, verás que Row actúa de manera similar a Column: de forma predeterminada, agrupa sus elementos secundarios a la izquierda (Column agrupaba sus elementos secundarios en la parte de arriba). Para corregir esto, podrías usar el mismo enfoque que antes, pero con mainAxisAlignment. Sin embargo, por motivos didácticos (y de aprendizaje), usa mainAxisSize. Esto le indica a Row que no debe ocupar todo el espacio horizontal disponible.

Realiza el siguiente cambio:

lib/main.dart

// ...

class MyHomePage extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    var appState = context.watch<MyAppState>();
    var pair = appState.current;

    return Scaffold(
      body: Center(
        child: Column(
          mainAxisAlignment: MainAxisAlignment.center,
          children: [
            BigCard(pair: pair),
            SizedBox(height: 10),
            Row(
              mainAxisSize: MainAxisSize.min,   // ← Add this.
              children: [
                ElevatedButton(
                  onPressed: () {
                    appState.getNext();
                  },
                  child: Text('Next'),
                ),
              ],
            ),
          ],
        ),
      ),
    );
  }
}

// ...

La IU volvió a estar como antes.

3d53d2b071e2f372.png

A continuación, agrega el botón Like y conéctalo a toggleFavorite(). Como desafío, primero intenta hacerlo por tu cuenta, sin mirar el bloque de código de más abajo.

e6b01a8c90df8ffa.png

No hay problema si no lo haces tal como está abajo. De hecho, no te preocupes por el ícono de corazón, a menos que de verdad quieras un desafío grande.

También está completamente bien si fallas (después de todo, es tu primera hora con Flutter).

252f7c4a212c94d2.png

Esta es una manera de agregar un segundo botón a MyHomePage. Esta vez, usa el constructor ElevatedButton.icon() para crear un botón con un ícono. En la parte superior del método build, elige el ícono apropiado en función de si el par actual de palabras ya se encuentra en los favoritos. Además, observa el uso de SizedBox una vez más, para mantener algo separados los dos botones.

lib/main.dart

// ...

class MyHomePage extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    var appState = context.watch<MyAppState>();
    var pair = appState.current;

    // ↓ Add this.
    IconData icon;
    if (appState.favorites.contains(pair)) {
      icon = Icons.favorite;
    } else {
      icon = Icons.favorite_border;
    }

    return Scaffold(
      body: Center(
        child: Column(
          mainAxisAlignment: MainAxisAlignment.center,
          children: [
            BigCard(pair: pair),
            SizedBox(height: 10),
            Row(
              mainAxisSize: MainAxisSize.min,
              children: [

                // ↓ And this.
                ElevatedButton.icon(
                  onPressed: () {
                    appState.toggleFavorite();
                  },
                  icon: Icon(icon),
                  label: Text('Like'),
                ),
                SizedBox(width: 10),

                ElevatedButton(
                  onPressed: () {
                    appState.getNext();
                  },
                  child: Text('Next'),
                ),
              ],
            ),
          ],
        ),
      ),
    );
  }
}

// ...

La app debería verse de la siguiente manera:

11981147e3497c77.gif

Lamentablemente, el usuario no puede ver los favoritos. Es hora de agregar una pantalla independiente a nuestra app. ¡Nos vemos en la próxima sección!

7. Agrega un riel de navegación

La mayoría de las apps no pueden incluir todo su contenido en una única pantalla. Esta app en particular probablemente podría, pero, por motivos didácticos, crearás una pantalla independiente para los favoritos del usuario. Para alternar entre las dos pantallas, implementarás tu primer StatefulWidget.

9320e50cad339e7b.png

Para ir al objetivo principal de este paso lo antes posible, divide MyHomePage en 2 widgets independientes.

Selecciona todo lo que está en MyHomePage, bórralo y reemplázalo con el siguiente código:

lib/main.dart

// ...

class MyHomePage extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      body: Row(
        children: [
          SafeArea(
            child: NavigationRail(
              extended: false,
              destinations: [
                NavigationRailDestination(
                  icon: Icon(Icons.home),
                  label: Text('Home'),
                ),
                NavigationRailDestination(
                  icon: Icon(Icons.favorite),
                  label: Text('Favorites'),
                ),
              ],
              selectedIndex: 0,
              onDestinationSelected: (value) {
                print('selected: $value');
              },
            ),
          ),
          Expanded(
            child: Container(
              color: Theme.of(context).colorScheme.primaryContainer,
              child: GeneratorPage(),
            ),
          ),
        ],
      ),
    );
  }
}

class GeneratorPage extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    var appState = context.watch<MyAppState>();
    var pair = appState.current;

    IconData icon;
    if (appState.favorites.contains(pair)) {
      icon = Icons.favorite;
    } else {
      icon = Icons.favorite_border;
    }

    return Center(
      child: Column(
        mainAxisAlignment: MainAxisAlignment.center,
        children: [
          BigCard(pair: pair),
          SizedBox(height: 10),
          Row(
            mainAxisSize: MainAxisSize.min,
            children: [
              ElevatedButton.icon(
                onPressed: () {
                  appState.toggleFavorite();
                },
                icon: Icon(icon),
                label: Text('Like'),
              ),
              SizedBox(width: 10),
              ElevatedButton(
                onPressed: () {
                  appState.getNext();
                },
                child: Text('Next'),
              ),
            ],
          ),
        ],
      ),
    );
  }
}

// ...

Cuando guardes, verás que el lado visual de la IU ya está listo, pero no funciona. Al hacer clic en ♥︎ (el corazón) del riel de navegación, no ocurre nada.

5a5a8e3a04789ce5.png

Revisa los cambios.

  • Primero, observa que el contenido completo de MyHomePage se extrajo en un widget nuevo, GeneratorPage. La única parte del widget de MyHomePage anterior que no se extrajo es Scaffold.
  • La nueva MyHomePage contiene una Row con dos elementos secundarios. El primer widget es SafeArea, y el segundo es un widget de Expanded.
  • SafeArea garantiza que sus elementos secundarios no se muestren oscurecidos por un recorte de hardware o una barra de estado. En esta app, el widget se une a NavigationRail para evitar que los botones de navegación se vean oscurecidos por una barra de estado para dispositivos móviles, por ejemplo.
  • Puedes cambiar la línea extended: false en NavigationRail a true. Esto mostrará las etiquetas junto a los íconos. En un paso futuro, aprenderás a hacer esto automáticamente cuando la app tenga suficiente espacio horizontal.
  • El riel de navegación tiene dos destinos (Home y Favorites), con sus respectivos íconos y etiquetas. También define el selectedIndex actual. Un índice seleccionado igual a cero selecciona el primer destino, uno igual a uno selecciona el segundo, y así sucesivamente. Por el momento, será un valor hard-coded igual a cero.
  • El riel de navegación también define qué ocurre cuando el usuario selecciona uno de los destinos con onDestinationSelected. Por ahora, la app solo mostrará el valor del índice requerido con print().
  • El segundo elemento secundario de la Row es el widget Expanded. Los widgets expandidos son sumamente útiles en filas y columnas: te permiten expresar diseños donde algunos elementos secundarios ocupan solo el espacio que necesitan (en este caso, NavigationRail) y otros widgets deberían ocupar tanto espacio del restante como sea posible (en este caso, Expanded). Una manera de pensar en los widgets Expanded es considerarlos "codiciosos". Si quieres conocer mejor el rol de este widget, une el widget de NavigationRail con otro Expanded. El diseño resultante se verá parecido al siguiente:

d80b73b692fb04c5.png

  • Dos widgets Expanded dividen todo el espacio horizontal entre ellos, incluso aunque el riel de navegación solamente necesite una pequeña porción en la parte izquierda.
  • Dentro del widget Expanded hay un Container de color y, dentro de este está el elemento GeneratorPage.

Widgets sin estado versus widgets con estado

Hasta ahora, MyAppState cubrió todas tus necesidades en términos de estado. Por esto, todos los widgets que escribiste hasta ahora son sin estado. No contienen ningún estado mutable propio. Ninguno de los widgets puede cambiarse a sí mismo: todos deben pasar por MyAppState.

Esto está a punto de cambiar.

Necesitas alguna manera de conservar el valor del selectedIndex del riel de navegación. También querrás cambiar este valor desde adentro de la devolución de llamada de onDestinationSelected.

Podrías agregar selectedIndex como una propiedad más de MyAppState. Y funcionaría. Pero puedes imaginar que el estado de la app rápidamente crecería más allá de lo razonable si cada widget almacenara sus valores en él.

e52d9c0937cc0823.jpeg

Algunos estados solo son relevantes para un único widget, de modo que debería quedarse con ese widget.

Ingresa el StatefulWidget, un tipo de widget que tiene State. Primero, convierte MyHomePage a un widget con estado.

Coloca el cursor en la primera línea de MyHomePage (la que empieza con class MyHomePage...) y despliega el menú Refactor usando Ctrl+. o Cmd+.. Luego, selecciona Convert to StatefulWidget.

238f98bceeb0de3a.gif

El IDE crea una nueva clase para ti, _MyHomePageState. Esta clase extiende State y, por lo tanto, puede administrar sus propios valores (puede cambiarse a sí misma). También observa que el método build del widget anterior y sin estado se movió a _MyHomePageState (en lugar de quedarse en el widget). Se movió palabra por palabra: nada de lo que estaba dentro del método build cambió. Ahora vive en otra parte.

setState

El nuevo widget con estado solamente necesita realizar el seguimiento de una variable: selectedIndex. Realiza los siguientes 3 cambios a _MyHomePageState:

lib/main.dart

// ...

class _MyHomePageState extends State<MyHomePage> {

  var selectedIndex = 0;     // ← Add this property.

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      body: Row(
        children: [
          SafeArea(
            child: NavigationRail(
              extended: false,
              destinations: [
                NavigationRailDestination(
                  icon: Icon(Icons.home),
                  label: Text('Home'),
                ),
                NavigationRailDestination(
                  icon: Icon(Icons.favorite),
                  label: Text('Favorites'),
                ),
              ],
              selectedIndex: selectedIndex,    // ← Change to this.
              onDestinationSelected: (value) {

                // ↓ Replace print with this.
                setState(() {
                  selectedIndex = value;
                });

              },
            ),
          ),
          Expanded(
            child: Container(
              color: Theme.of(context).colorScheme.primaryContainer,
              child: GeneratorPage(),
            ),
          ),
        ],
      ),
    );
  }
}

// ...

Revisa los cambios:

  1. Establecerás una nueva variable, selectedIndex, y la inicializarás en 0.
  2. Usarás esta nueva variable en la definición de NavigationRail en lugar de usar el valor hard-coded 0 que estaba allí hasta ahora.
  3. Cuando se llame a la devolución de llamada onDestinationSelected, en lugar de solo imprimir el valor nuevo en la consola, lo asignarás a selectedIndex dentro de una llamada a setState(). Esta llamada es similar al método notifyListeners() que usamos antes: se asegura de que la IU se actualice.

2b31dd91c5ba6766.gif

El riel de navegación ahora responde a la interacción del usuario. Sin embargo, el área expandida de la derecha no cambió. Eso se debe a que el código no está usando selectedIndex para determinar qué pantalla muestra los datos.

Usa selectedIndex

Coloca el siguiente código en la parte superior del método build de _MyHomePageState, justo antes de return Scaffold:

lib/main.dart

// ...

Widget page;
switch (selectedIndex) {
  case 0:
    page = GeneratorPage();
    break;
  case 1:
    page = Placeholder();
    break;
  default:
    throw UnimplementedError('no widget for $selectedIndex');
}

// ...

Revisa esta parte del código:

  1. El código declara una nueva variable, page, de tipo Widget.
  2. Luego, una sentencia switch asigna una pantalla a page, según el valor actual de selectedIndex.
  3. Dado que FavoritesPage todavía no existe, usa Placeholder, un widget útil que dibuja un rectángulo tachado en el lugar en el que lo coloques, lo que indica que esa parte de la IU no está terminada.

5685cf886047f6ec.png

  1. En virtud del principio de fracasar rápido, la sentencia switch también se asegura de mostrar un error si selectedIndex no es ni 0 ni 1. Esto ayudará a evitar errores en procesos futuros. Si alguna vez agregas un destino nuevo al riel de navegación y olvidas actualizar este código, el programa fallará en el desarrollo (en lugar de dejarte pensando por qué las cosas no funcionan o permitirte publicar un código con errores en producción).

Ahora que page contiene el widget que deseas mostrar en la derecha, seguramente puedas adivinar el otro cambio necesario.

Así se ve _MyHomePageState luego de ese único cambio faltante:

lib/main.dart

// ...

class _MyHomePageState extends State<MyHomePage> {
  var selectedIndex = 0;

  @override
  Widget build(BuildContext context) {
    Widget page;
    switch (selectedIndex) {
      case 0:
        page = GeneratorPage();
        break;
      case 1:
        page = Placeholder();
        break;
      default:
        throw UnimplementedError('no widget for $selectedIndex');
    }

    return Scaffold(
      body: Row(
        children: [
          SafeArea(
            child: NavigationRail(
              extended: false,
              destinations: [
                NavigationRailDestination(
                  icon: Icon(Icons.home),
                  label: Text('Home'),
                ),
                NavigationRailDestination(
                  icon: Icon(Icons.favorite),
                  label: Text('Favorites'),
                ),
              ],
              selectedIndex: selectedIndex,
              onDestinationSelected: (value) {
                setState(() {
                  selectedIndex = value;
                });
              },
            ),
          ),
          Expanded(
            child: Container(
              color: Theme.of(context).colorScheme.primaryContainer,
              child: page,  // ← Here.
            ),
          ),
        ],
      ),
    );
  }
}

// ...

La app ahora cambia entre nuestra GeneratorPage y el marcador de posición que pronto se convertirá en la página Favorites.

4122ee1c4830e0eb.gif

Capacidad de respuesta

A continuación, hagamos que el riel de navegación sea responsivo. Es decir, que muestre automáticamente las etiquetas (usando extended: true) cuando haya suficiente espacio para ellas.

bef3378cb73f9a40.png

Flutter brinda varios widgets que te ayudarán a hacer que tu app sea responsiva automáticamente. Por ejemplo, Wrap es un widget similar a Row o Column que automáticamente une elementos secundarios a la próxima "línea" (llamada "ejecución") cuando no hay suficiente espacio vertical u horizontal. FittedBox es un widget que automáticamente incluye su elemento secundario en el espacio disponible según tus especificaciones.

Pero NavigationRail no muestra automáticamente las etiquetas cuando hay suficiente espacio, ya que no puede saber qué es suficiente espacio en cada contexto. Esa decisión depende de ti, el desarrollador.

Digamos que decides mostrar las etiquetas solamente si MyHomePage tiene un ancho mínimo de 600 píxeles.

En este caso, el widget que usaremos es LayoutBuilder. Te permitirá cambiar tu árbol de widgets en función del espacio disponible que haya.

Una vez más, usa el menú Refactor de Flutter en VS Code para realizar los cambios deseados. Sin embargo, esta vez, es un poco más complicado:

  1. Dentro del método build de _MyHomePageState, coloca el cursor en Scaffold.
  2. Despliega el menú Refactor con Ctrl+. (Windows/Linux) o Cmd+. (Mac).
  3. Selecciona Wrap with Builder y presiona Intro.
  4. Modifica el nombre del Builder agregado recientemente por LayoutBuilder.
  5. Cambia la lista de parámetros de devolución de llamada de (context) a (context, constraints).

52d18742c54f1022.gif

Cada vez que las restricciones cambian, se llama a la devolución de llamada builder de LayoutBuilder. Esto ocurre, por ejemplo, en las siguientes situaciones:

  • El usuario cambia el tamaño de la ventana de la app.
  • El usuario rota el teléfono de modo vertical a modo horizontal, o viceversa.
  • Algún widget junto a MyHomePage aumenta de tamaño, lo que hace que las restricciones de MyHomePage resulten más pequeñas.
  • Etcétera.

Ahora, tu código podrá decidir si mostrar la etiqueta consultando las constraints actuales. Haz el siguiente cambio de una línea en el método build de _MyHomePageState:

lib/main.dart

// ...

class _MyHomePageState extends State<MyHomePage> {
  var selectedIndex = 0;

  @override
  Widget build(BuildContext context) {
    Widget page;
    switch (selectedIndex) {
      case 0:
        page = GeneratorPage();
        break;
      case 1:
        page = Placeholder();
        break;
      default:
        throw UnimplementedError('no widget for $selectedIndex');
    }

    return LayoutBuilder(builder: (context, constraints) {
      return Scaffold(
        body: Row(
          children: [
            SafeArea(
              child: NavigationRail(
                extended: constraints.maxWidth >= 600,  // ← Here.
                destinations: [
                  NavigationRailDestination(
                    icon: Icon(Icons.home),
                    label: Text('Home'),
                  ),
                  NavigationRailDestination(
                    icon: Icon(Icons.favorite),
                    label: Text('Favorites'),
                  ),
                ],
                selectedIndex: selectedIndex,
                onDestinationSelected: (value) {
                  setState(() {
                    selectedIndex = value;
                  });
                },
              ),
            ),
            Expanded(
              child: Container(
                color: Theme.of(context).colorScheme.primaryContainer,
                child: page,
              ),
            ),
          ],
        ),
      );
    });
  }
}

// ...

Ahora, tu app responde a su entorno, como el tamaño de la pantalla, la orientación y la plataforma. En otras palabras, es responsiva.

6223bd3e2dc157eb.gif

Lo único que resta por hacer es reemplazar ese Placeholder con una pantalla Favorites real. Abordaremos esto en la próxima sección.

8. Agrega una nueva página

¿Recuerdas el widget de Placeholder que usamos en lugar de la página Favorites?

4122ee1c4830e0eb.gif

Es hora de corregir esto.

Si te sientes audaz, intenta hacer esto por tu cuenta. Tu objetivo es mostrar la lista de favorites en un widget nuevo y sin estado, FavoritesPage, y luego mostrar ese widget en lugar del Placeholder.

Estos son algunos consejos:

  • Cuando quieras una Column que se desplace, usa el widget de ListView.
  • Recuerda: accede a la instancia de MyAppState desde cualquier widget usando context.watch<MyAppState>().
  • Si también quieres probar un widget nuevo, ListTile tiene propiedades como title (en general, para texto), leading (para íconos y avatares) y onTap (para interacciones). Sin embargo, puedes obtener efectos similares con los widgets que ya conoces.
  • Dart permite el uso de bucles for dentro de los literales de la colección. Por ejemplo, si messages contiene una lista de cadenas, puedes tener un código como el que se indica a continuación:

f0444bba08f205aa.png

Por otro lado, si tienes más conocimientos sobre programación funcional, Dart también te permite escribir código como messages.map((m) => Text(m)).toList(). Y, por supuesto, siempre puedes crear una lista de widgets y agregarle contenido de forma imperativa dentro del método build.

La ventaja de agregar la página Favorites por tu cuenta es que aprenderás más tomando tus propias decisiones. La desventaja es que quizás te encuentres ante problemas que aún no puedas resolver de forma autónoma. Recuerda: fracasar está bien y es uno de los elementos más importantes del aprendizaje. Nadie espera que entiendas el desarrollo de Flutter en tu primera hora, y tú tampoco deberías esperar eso.

252f7c4a212c94d2.png

Lo que sigue es solo una manera de implementar la página de favoritos. La forma en que se implementará te inspirará (o eso esperamos) a que juegues con el código: mejora la IU y personalízala.

Esta es la clase FavoritesPage nueva:

lib/main.dart

// ...

class FavoritesPage extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    var appState = context.watch<MyAppState>();

    if (appState.favorites.isEmpty) {
      return Center(
        child: Text('No favorites yet.'),
      );
    }

    return ListView(
      children: [
        Padding(
          padding: const EdgeInsets.all(20),
          child: Text('You have '
              '${appState.favorites.length} favorites:'),
        ),
        for (var pair in appState.favorites)
          ListTile(
            leading: Icon(Icons.favorite),
            title: Text(pair.asLowerCase),
          ),
      ],
    );
  }
}

Esto es lo que hace el widget:

  • Obtiene el estado actual de la app.
  • Si la lista de favoritos está vacía, muestra un mensaje centrado que indica No favorites yet*.*
  • De lo contrario, muestra una lista (por la que el usuario se puede desplazar).
  • La lista comienza con un resumen (por ejemplo, Tienes 5 favoritos*.*).
  • Luego, el código itera por todos los favoritos y construye un widget de ListTile para cada uno.

Todo lo que resta ahora es reemplazar el widget de Placeholder con una FavoritesPage. ¡Listo!

1d26af443561f39c.gif

Puedes obtener el código final de esta app en el repositorio del codelab en GitHub.

9. Próximos pasos

¡Felicitaciones!

¡Qué bien lo hiciste! Utilizaste un andamiaje no funcional con un widget de Column y dos widgets de Text, y obtuviste una pequeña app responsiva y encantadora.

d6e3d5f736411f13.png

Temas abordados

  • Cuáles son los conceptos básicos del funcionamiento de Flutter
  • Cómo crear diseños en Flutter
  • Cómo conectar las interacciones del usuario (como la presión de un botón) con el comportamiento de la app
  • Cómo mantener organizado tu código de Flutter
  • Cómo hacer que tu app sea responsiva
  • Cómo lograr que tu app tenga un aspecto y una experiencia coherentes

¿Qué debes hacer a continuación?

  • Experimenta un poco más con la app que escribiste en este lab.
  • Consulta el código de esta versión avanzada de la misma app para ver cómo puedes agregar listas animadas, gradientes, atenuaciones de transición y mucho más.

d4afd1f43ab976f7.gif