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:
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.
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".
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.
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:
- El SDK de Flutter
- Visual Studio Code con el complemento de Flutter
- 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
- ¿Cómo encuentro la ruta de acceso al SDK de Flutter?
- ¿Qué debo hacer si no encuentro el comando de Flutter?
- ¿Cómo soluciono el problema que indica "Waiting for another flutter command to release the startup lock"?
- ¿Cómo le indico a Flutter la ubicación de mi instalación del SDK de Android?
- ¿Cómo corrijo el error de Java cuando ejecuto
flutter doctor --android-licenses
? - ¿Cómo corrijo el error que indica que no se encontró la herramienta
sdkmanager
de Android? - ¿Cómo corrijo el error que indica "
cmdline-tools
component is missing"? - ¿Cómo ejecuto CocoaPods en Apple Silicon (M1)?
- ¿Cómo puedo inhabilitar la aplicación automática del formato en el momento de guardar en VS Code?
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.
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
.
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
.
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
.
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/
.
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.
Mientras lib/main.dart
está abierto, busca el botón de "reproducir" en el extremo superior derecho de la ventana de VS Code y haz clic en él.
Aproximadamente un minuto después, se iniciará tu app en modo de depuración. No parece gran cosa todavía:
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.
Preguntas frecuentes
- ¿Qué ocurre si no funciona la recarga en caliente en VS Code?
- ¿Debo presionar "r" para hacer una recarga en caliente en VS Code?
- ¿Funciona la recarga en caliente en la Web?
- ¿Cómo quito el banner "Debug"?
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.
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 enMyApp
). Esto le permite a cualquier widget de la app obtener el estado.
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:
- 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. MyHomePage
realiza el seguimiento del estado actual de la app usando el métodowatch
.- 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 esScaffold
. No vas a trabajar conScaffold
en este codelab, pero es un widget útil y se encuentra en la gran mayoría de las apps de Flutter del mundo real. 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.- Cambiaste este widget de
Text
en el primer paso. - Este segundo widget de
Text
toma elappState
y accede al único miembro de esa clase,current
(que es unWordPair
).WordPair
proporciona varios métodos get de utilidad, comoasPascalCase
oasSnakeCase
. Aquí, usaremosasLowerCase
, pero puedes cambiar esto ahora si prefieres alguna otra alternativa. - 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 deColumn
. 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.
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:
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:
- 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
- Mueve el cursor hacia la parte del código que quieres refactorizar (en este caso,
Text
) y presionaCtrl+.
(Windows/Linux) oCmd+.
(Mac).
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.
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.
Esto une el widget de Padding
, y, por lo tanto, también el Text
, con un widget de Card
.
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, yprimary
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:
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í.
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 comobodyMedium
(para texto estándar de tamaño mediano),caption
(para leyendas de imágenes) oheadlineLarge
(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 dedisplayMedium
, 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 sernull
. 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 sernull
. 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()
endisplayMedium
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:
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 decopyWith()
y presionaCtrl+Shift+Space
(Windows/Linux) oCmd+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ámetroelevation
. - 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 deonPrimary
.
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.
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).
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.
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.
La app debería tener un aspecto similar al siguiente:
Si lo deseas, puedes hacer algunos ajustes más.
- Puedes quitar el widget de
Text
que se encuentra sobreBigCard
. 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)
entreBigCard
yElevatedButton
. De esta forma, habrá un poco más de espacio de separación entre los dos widgets. El widget deSizedBox
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:
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".
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
llamadafavorites
. 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 deWordPair
a ella. A su vez, puedes usar la lista defavorites
a sabiendas de que nunca podrá haber allí objetos no deseados (comonull
).
- 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 anotifyListeners();
.
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.
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.
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.
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).
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:
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
.
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.
Revisa los cambios.
- Primero, observa que el contenido completo de
MyHomePage
se extrajo en un widget nuevo,GeneratorPage
. La única parte del widget deMyHomePage
anterior que no se extrajo esScaffold
. - La nueva
MyHomePage
contiene unaRow
con dos elementos secundarios. El primer widget esSafeArea
, y el segundo es un widget deExpanded
. 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 aNavigationRail
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 atrue
. 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 conprint()
. - El segundo elemento secundario de la
Row
es el widgetExpanded
. 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 widgetsExpanded
es considerarlos "codiciosos". Si quieres conocer mejor el rol de este widget, une el widget deNavigationRail
con otroExpanded
. El diseño resultante se verá parecido al siguiente:
- 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 unContainer
de color y, dentro de este está el elementoGeneratorPage
.
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.
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.
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:
- Establecerás una nueva variable,
selectedIndex
, y la inicializarás en0
. - Usarás esta nueva variable en la definición de
NavigationRail
en lugar de usar el valor hard-coded0
que estaba allí hasta ahora. - Cuando se llame a la devolución de llamada
onDestinationSelected
, en lugar de solo imprimir el valor nuevo en la consola, lo asignarás aselectedIndex
dentro de una llamada asetState()
. Esta llamada es similar al métodonotifyListeners()
que usamos antes: se asegura de que la IU se actualice.
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:
- El código declara una nueva variable,
page
, de tipoWidget
. - Luego, una sentencia switch asigna una pantalla a
page
, según el valor actual deselectedIndex
. - Dado que
FavoritesPage
todavía no existe, usaPlaceholder
, 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.
- 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.
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.
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:
- Dentro del método
build
de_MyHomePageState
, coloca el cursor enScaffold
. - Despliega el menú Refactor con
Ctrl+.
(Windows/Linux) oCmd+.
(Mac). - Selecciona Wrap with Builder y presiona Intro.
- Modifica el nombre del
Builder
agregado recientemente porLayoutBuilder
. - Cambia la lista de parámetros de devolución de llamada de
(context)
a(context, constraints)
.
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 deMyHomePage
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.
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?
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 deListView
. - Recuerda: accede a la instancia de
MyAppState
desde cualquier widget usandocontext.watch<MyAppState>()
. - Si también quieres probar un widget nuevo,
ListTile
tiene propiedades comotitle
(en general, para texto),leading
(para íconos y avatares) yonTap
(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, simessages
contiene una lista de cadenas, puedes tener un código como el que se indica a continuación:
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.
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!
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.
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.
- Continúa tu recorrido de aprendizaje en flutter.dev/learn.