1. Antes de comenzar
En este codelab, aprenderás algunos conceptos básicos de Firebase para crear apps para dispositivos móviles de Flutter para Android y iOS.
Requisitos previos
- Conocimientos de Flutter
- El SDK de Flutter
- Un editor de texto de tu elección
Qué aprenderás
- Cómo compilar una app de chat de RSVP y libro de visitas para eventos en Android, iOS, la Web y macOS con Flutter
- Cómo autenticar usuarios con Firebase Authentication y sincronizar datos con Firestore
Requisitos
Cualquiera de los siguientes dispositivos:
- Un dispositivo físico Android o iOS conectado a tu computadora y configurado en el modo de desarrollador
- El simulador de iOS (requiere herramientas de Xcode)
- Android Emulator (requiere configuración en Android Studio)
También necesitas lo siguiente:
- Un navegador de tu elección, como Google Chrome
- Un IDE o editor de texto que prefieras, configurado con los complementos de Dart y Flutter, como Android Studio o Visual Studio Code
- La versión
stable
más reciente de Flutter obeta
si te gusta vivir al límite - Una Cuenta de Google para crear y administrar tu proyecto de Firebase
- La CLI de
Firebase
accedió a tu Cuenta de Google.
2. Obtén el código de muestra
Descarga la versión inicial de tu proyecto desde GitHub:
- Desde la línea de comandos, clona el repositorio de GitHub en el directorio
flutter-codelabs
:
git clone https://github.com/flutter/codelabs.git flutter-codelabs
El directorio flutter-codelabs
contiene el código de una colección de codelabs. El código de este codelab se encuentra en el directorio flutter-codelabs/firebase-get-to-know-flutter
. El directorio contiene una serie de instantáneas que muestran cómo debería verse tu proyecto al final de cada paso. Por ejemplo, estás en el segundo paso.
- Busca los archivos coincidentes para el segundo paso:
cd flutter-codelabs/firebase-get-to-know-flutter/step_02
Si quieres avanzar o ver cómo debería verse algo después de un paso, busca en el directorio que lleva el nombre del paso que te interesa.
Importa la app de partida
- Abre o importa el directorio
flutter-codelabs/firebase-get-to-know-flutter/step_02
en tu IDE preferido. Este directorio contiene el código de inicio para el codelab, que consiste en una app de reuniones de Flutter que todavía no es funcional.
Cómo ubicar los archivos que necesitan trabajo
El código de esta app se distribuye en varios directorios. Esta división de la funcionalidad facilita el trabajo porque agrupa el código por funcionalidad.
- Ubica los siguientes archivos:
lib/main.dart
: Este archivo contiene el punto de entrada principal y el widget de la app.lib/home_page.dart
: Este archivo contiene el widget de la página principal.lib/src/widgets.dart
: Este archivo contiene algunos widgets para ayudar a estandarizar el estilo de la app. Estos componen la pantalla de la app de inicio.lib/src/authentication.dart
: Este archivo contiene una implementación parcial de Authentication con un conjunto de widgets para crear una experiencia de acceso del usuario para la autenticación basada en correo electrónico de Firebase. Estos widgets para el flujo de autenticación aún no se usan en la app de inicio, pero los agregarás pronto.
Agrega archivos adicionales según sea necesario para compilar el resto de la app.
Revisa el archivo lib/main.dart
Esta app aprovecha el paquete google_fonts
para que Roboto sea la fuente predeterminada en toda la app. Puedes explorar fonts.google.com y usar las fuentes que descubras allí en diferentes partes de la app.
Usas los widgets de ayuda del archivo lib/src/widgets.dart
en forma de Header
, Paragraph
y IconAndDetail
. Estos widgets eliminan el código duplicado para reducir el desorden en el diseño de la página que se describe en HomePage
. Esto también permite una apariencia y un funcionamiento coherentes.
Así se ve tu app en Android, iOS, la Web y macOS:
3. Crea y configura un proyecto de Firebase
La visualización de la información del evento es excelente para tus invitados, pero no es muy útil por sí sola. Debes agregar algunas funciones dinámicas a la app. Para ello, debes conectar Firebase a tu app. Para comenzar con Firebase, debes crear y configurar un proyecto de Firebase.
Crea un proyecto de Firebase
- Accede a la consola de Firebase con tu Cuenta de Google.
- Haz clic en el botón para crear un proyecto nuevo y, luego, ingresa un nombre (por ejemplo,
Firebase-Flutter-Codelab
).
- Haz clic en Continuar.
- Si se te solicita, revisa y acepta las Condiciones de Firebase y, luego, haz clic en Continuar.
- (Opcional) Habilita la asistencia de IA en Firebase console (llamada "Gemini en Firebase").
- Para este codelab, no necesitas Google Analytics, por lo que debes desactivar la opción de Google Analytics.
- Haz clic en Crear proyecto, espera a que se aprovisione y, luego, haz clic en Continuar.
Para obtener más información sobre los proyectos de Firebase, consulta Información sobre los proyectos de Firebase.
Configura los productos de Firebase
La app usa los siguientes productos de Firebase, que están disponibles para aplicaciones web:
- Authentication: Permite que los usuarios accedan a tu app.
- Firestore: Guarda datos estructurados en la nube y recibe notificaciones instantáneas cuando se modifican los datos.
- Reglas de seguridad de Firebase: Protegen tu base de datos.
Algunos de estos productos necesitan una configuración especial o debes habilitarlos en Firebase console.
Habilita la autenticación de acceso con correo electrónico
- En el panel Descripción general del proyecto de Firebase console, expande el menú Compilación.
- Haz clic en Authentication > Get Started > Sign-in method > Email/Password > Enable > Save.
Configura Firestore
La app web usa Firestore para guardar los mensajes de chat y recibir mensajes nuevos.
Sigue estos pasos para configurar Firestore en tu proyecto de Firebase:
- En el panel izquierdo de Firebase console, expande Compilación y, luego, selecciona Base de datos de Firestore.
- Haz clic en Crear base de datos.
- Deja el ID de la base de datos establecido en
(default)
. - Selecciona una ubicación para tu base de datos y, luego, haz clic en Siguiente.
Para una app real, debes elegir una ubicación cercana a tus usuarios. - Haz clic en Comenzar en modo de prueba. Lee la renuncia de responsabilidad sobre las reglas de seguridad.
Más adelante en este codelab, agregarás reglas de seguridad para proteger tus datos. No distribuyas ni expongas una app de forma pública sin agregar reglas de seguridad para tu base de datos. - Haz clic en Crear.
4. Configura Firebase
Para usar Firebase con Flutter, debes completar las siguientes tareas para configurar el proyecto de Flutter de modo que use las bibliotecas de FlutterFire
correctamente:
- Agrega las dependencias de
FlutterFire
a tu proyecto. - Registra la plataforma deseada en el proyecto de Firebase.
- Descarga el archivo de configuración específico de la plataforma y, luego, agrégalo al código.
En el directorio de nivel superior de tu app de Flutter, hay subdirectorios android
, ios
, macos
y web
, que contienen los archivos de configuración específicos de la plataforma para iOS y Android, respectivamente.
Configura dependencias
Debes agregar las bibliotecas de FlutterFire
para los dos productos de Firebase que usas en esta app: Authentication y Firestore.
- En la línea de comandos, agrega las siguientes dependencias:
$ flutter pub add firebase_core
El paquete firebase_core
es el código común que requieren todos los complementos de Firebase para Flutter.
$ flutter pub add firebase_auth
El paquete firebase_auth
permite la integración con Authentication.
$ flutter pub add cloud_firestore
El paquete cloud_firestore
permite el acceso al almacenamiento de datos de Firestore.
$ flutter pub add provider
El paquete firebase_ui_auth
proporciona un conjunto de widgets y utilidades para aumentar la velocidad de desarrollo con flujos de autenticación.
$ flutter pub add firebase_ui_auth
Agregaste los paquetes necesarios, pero también debes configurar los proyectos de Runner para iOS, Android, macOS y la Web para que usen Firebase de forma adecuada. También usas el paquete provider
que permite separar la lógica empresarial de la lógica de visualización.
Instala la CLI de FlutterFire
La CLI de FlutterFire depende de la CLI de Firebase subyacente.
- Si aún no lo hiciste, instala la CLI de Firebase en tu máquina.
- Instala la CLI de FlutterFire:
$ dart pub global activate flutterfire_cli
Una vez instalado, el comando flutterfire
estará disponible de forma global.
Configura tus apps
La CLI extrae información de tu proyecto de Firebase y de las apps del proyecto seleccionadas para generar toda la configuración de una plataforma específica.
En la raíz de tu app, ejecuta el comando configure
:
$ flutterfire configure
El comando de configuración te guía por los siguientes procesos:
- Selecciona un proyecto de Firebase basado en el archivo
.firebaserc
o en Firebase console. - Determina las plataformas para la configuración, como Android, iOS, macOS y la Web.
- Identifica las apps de Firebase desde las que se extraerá la configuración. De forma predeterminada, la CLI intenta establecer coincidencias automáticamente entre las apps de Firebase según la configuración actual del proyecto.
- Genera un archivo
firebase_options.dart
en tu proyecto.
Configura macOS
Flutter en macOS compila apps completamente aisladas. Como esta app se integra en la red para comunicarse con los servidores de Firebase, debes configurarla con privilegios de cliente de red.
macos/Runner/DebugProfile.entitlements
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>com.apple.security.app-sandbox</key>
<true/>
<key>com.apple.security.cs.allow-jit</key>
<true/>
<key>com.apple.security.network.server</key>
<true/>
<!-- Add the following two lines -->
<key>com.apple.security.network.client</key>
<true/>
</dict>
</plist>
macos/Runner/Release.entitlements
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>com.apple.security.app-sandbox</key>
<true/>
<!-- Add the following two lines -->
<key>com.apple.security.network.client</key>
<true/>
</dict>
</plist>
Para obtener más información, consulta Compatibilidad con Flutter para computadoras.
5. Agrega la función de confirmación de asistencia
Ahora que agregaste Firebase a la app, puedes crear un botón de RSVP que registre a las personas con Authentication. En el caso de Android nativo, iOS nativo y la Web, existen paquetes FirebaseUI Auth
prediseñados, pero debes compilar esta capacidad para Flutter.
El proyecto que recuperaste antes incluía un conjunto de widgets que implementan la interfaz de usuario para la mayor parte del flujo de autenticación. Implementarás la lógica empresarial para integrar la autenticación con la app.
Agrega lógica empresarial con el paquete Provider
Usa el paquete provider
para que un objeto de estado de la app centralizado esté disponible en todo el árbol de widgets de Flutter de la app:
- Crea un nuevo archivo llamado
app_state.dart
con el siguiente contenido:
lib/app_state.dart
import 'package:firebase_auth/firebase_auth.dart'
hide EmailAuthProvider, PhoneAuthProvider;
import 'package:firebase_core/firebase_core.dart';
import 'package:firebase_ui_auth/firebase_ui_auth.dart';
import 'package:flutter/material.dart';
import 'firebase_options.dart';
class ApplicationState extends ChangeNotifier {
ApplicationState() {
init();
}
bool _loggedIn = false;
bool get loggedIn => _loggedIn;
Future<void> init() async {
await Firebase.initializeApp(
options: DefaultFirebaseOptions.currentPlatform);
FirebaseUIAuth.configureProviders([
EmailAuthProvider(),
]);
FirebaseAuth.instance.userChanges().listen((user) {
if (user != null) {
_loggedIn = true;
} else {
_loggedIn = false;
}
notifyListeners();
});
}
}
Las instrucciones import
introducen Firebase Core y Auth, incorporan el paquete provider
que hace que el objeto de estado de la app esté disponible en todo el árbol de widgets y, además, incluyen los widgets de autenticación del paquete firebase_ui_auth
.
Este objeto de estado de la aplicación ApplicationState
tiene una responsabilidad principal para este paso, que es alertar al árbol de widgets de que hubo una actualización en un estado autenticado.
Solo usas un proveedor para comunicar el estado de acceso de un usuario a la app. Para permitir que un usuario acceda, usas las IU proporcionadas por el paquete firebase_ui_auth
, que es una excelente manera de iniciar rápidamente las pantallas de acceso en tus apps.
Integra el flujo de autenticación
- Modifica las importaciones en la parte superior del archivo
lib/main.dart
:
lib/main.dart
import 'package:firebase_ui_auth/firebase_ui_auth.dart'; // new
import 'package:flutter/material.dart';
import 'package:go_router/go_router.dart'; // new
import 'package:google_fonts/google_fonts.dart';
import 'package:provider/provider.dart'; // new
import 'app_state.dart'; // new
import 'home_page.dart';
- Conecta el estado de la app con la inicialización de la app y, luego, agrega el flujo de autenticación a
HomePage
:
lib/main.dart
void main() {
// Modify from here...
WidgetsFlutterBinding.ensureInitialized();
runApp(ChangeNotifierProvider(
create: (context) => ApplicationState(),
builder: ((context, child) => const App()),
));
// ...to here.
}
La modificación de la función main()
hace que el paquete del proveedor sea responsable de la creación de instancias del objeto de estado de la app con el widget ChangeNotifierProvider
. Usas esta clase provider
específica porque el objeto de estado de la app extiende la clase ChangeNotifier
, lo que permite que el paquete provider
sepa cuándo volver a mostrar los widgets dependientes.
- Actualiza tu app para controlar la navegación a diferentes pantallas que FirebaseUI te proporciona creando una configuración de
GoRouter
:
lib/main.dart
// Add GoRouter configuration outside the App class
final _router = GoRouter(
routes: [
GoRoute(
path: '/',
builder: (context, state) => const HomePage(),
routes: [
GoRoute(
path: 'sign-in',
builder: (context, state) {
return SignInScreen(
actions: [
ForgotPasswordAction(((context, email) {
final uri = Uri(
path: '/sign-in/forgot-password',
queryParameters: <String, String?>{
'email': email,
},
);
context.push(uri.toString());
})),
AuthStateChangeAction(((context, state) {
final user = switch (state) {
SignedIn state => state.user,
UserCreated state => state.credential.user,
_ => null
};
if (user == null) {
return;
}
if (state is UserCreated) {
user.updateDisplayName(user.email!.split('@')[0]);
}
if (!user.emailVerified) {
user.sendEmailVerification();
const snackBar = SnackBar(
content: Text(
'Please check your email to verify your email address'));
ScaffoldMessenger.of(context).showSnackBar(snackBar);
}
context.pushReplacement('/');
})),
],
);
},
routes: [
GoRoute(
path: 'forgot-password',
builder: (context, state) {
final arguments = state.uri.queryParameters;
return ForgotPasswordScreen(
email: arguments['email'],
headerMaxExtent: 200,
);
},
),
],
),
GoRoute(
path: 'profile',
builder: (context, state) {
return ProfileScreen(
providers: const [],
actions: [
SignedOutAction((context) {
context.pushReplacement('/');
}),
],
);
},
),
],
),
],
);
// end of GoRouter configuration
// Change MaterialApp to MaterialApp.router and add the routerConfig
class App extends StatelessWidget {
const App({super.key});
@override
Widget build(BuildContext context) {
return MaterialApp.router(
title: 'Firebase Meetup',
theme: ThemeData(
buttonTheme: Theme.of(context).buttonTheme.copyWith(
highlightColor: Colors.deepPurple,
),
primarySwatch: Colors.deepPurple,
textTheme: GoogleFonts.robotoTextTheme(
Theme.of(context).textTheme,
),
visualDensity: VisualDensity.adaptivePlatformDensity,
useMaterial3: true,
),
routerConfig: _router, // new
);
}
}
Cada pantalla tiene un tipo de acción diferente asociada según el nuevo estado del flujo de autenticación. Después de la mayoría de los cambios de estado en la autenticación, puedes volver a enrutar a una pantalla preferida, ya sea la pantalla principal o una diferente, como la de perfil.
- En el método de compilación de la clase
HomePage
, integra el estado de la app con el widgetAuthFunc
:
lib/home_page.dart
import 'package:firebase_auth/firebase_auth.dart' // new
hide EmailAuthProvider, PhoneAuthProvider; // new
import 'package:flutter/material.dart'; // new
import 'package:provider/provider.dart'; // new
import 'app_state.dart'; // new
import 'src/authentication.dart'; // new
import 'src/widgets.dart';
class HomePage extends StatelessWidget {
const HomePage({super.key});
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text('Firebase Meetup'),
),
body: ListView(
children: <Widget>[
Image.asset('assets/codelab.png'),
const SizedBox(height: 8),
const IconAndDetail(Icons.calendar_today, 'October 30'),
const IconAndDetail(Icons.location_city, 'San Francisco'),
// Add from here
Consumer<ApplicationState>(
builder: (context, appState, _) => AuthFunc(
loggedIn: appState.loggedIn,
signOut: () {
FirebaseAuth.instance.signOut();
}),
),
// to here
const Divider(
height: 8,
thickness: 1,
indent: 8,
endIndent: 8,
color: Colors.grey,
),
const Header("What we'll be doing"),
const Paragraph(
'Join us for a day full of Firebase Workshops and Pizza!',
),
],
),
);
}
}
Creas una instancia del widget AuthFunc
y la encapsulas en un widget Consumer
. El widget Consumer es la forma habitual en que se puede usar el paquete provider
para volver a compilar parte del árbol cuando cambia el estado de la app. El widget AuthFunc
son los widgets complementarios que pruebas.
Prueba el flujo de autenticación
- En la app, presiona el botón Confirmar asistencia para iniciar la
SignInScreen
.
- Ingresa una dirección de correo electrónico. Si ya te registraste, el sistema te pedirá que ingreses una contraseña. De lo contrario, el sistema te pedirá que completes el formulario de registro.
- Ingresa una contraseña de menos de seis caracteres para verificar el flujo de control de errores. Si te registraste, verás la contraseña de.
- Ingresa contraseñas incorrectas para verificar el flujo de control de errores.
- Ingresa la contraseña correcta. Verás la experiencia de acceso, que le ofrece al usuario la posibilidad de salir de su cuenta.
6. Escribe mensajes en Firestore
Es genial saber que los usuarios están llegando, pero debes darles a los invitados algo más para hacer en la app. ¿Qué tal si pudieran dejar mensajes en un libro de visitas? Pueden compartir por qué les entusiasma asistir o a quién esperan conocer.
Para almacenar los mensajes de chat que escriben los usuarios en la app, usas Firestore.
Modelo de datos
Firestore es una base de datos NoSQL, y los datos almacenados en ella se dividen en colecciones, documentos, campos y subcolecciones. Almacenas cada mensaje del chat como un documento en una colección guestbook
, que es una colección de nivel superior.
Agregue mensajes a Firestore
En esta sección, agregarás la funcionalidad para que los usuarios escriban mensajes en la base de datos. Primero, agregarás un campo de formulario y un botón de envío, y, luego, agregarás el código que conecta estos elementos con la base de datos.
- Crea un archivo nuevo llamado
guest_book.dart
y agrega un widget con estadoGuestBook
para construir los elementos de la IU de un campo de mensaje y un botón de envío:
lib/guest_book.dart
import 'dart:async';
import 'package:flutter/material.dart';
import 'src/widgets.dart';
class GuestBook extends StatefulWidget {
const GuestBook({required this.addMessage, super.key});
final FutureOr<void> Function(String message) addMessage;
@override
State<GuestBook> createState() => _GuestBookState();
}
class _GuestBookState extends State<GuestBook> {
final _formKey = GlobalKey<FormState>(debugLabel: '_GuestBookState');
final _controller = TextEditingController();
@override
Widget build(BuildContext context) {
return Padding(
padding: const EdgeInsets.all(8.0),
child: Form(
key: _formKey,
child: Row(
children: [
Expanded(
child: TextFormField(
controller: _controller,
decoration: const InputDecoration(
hintText: 'Leave a message',
),
validator: (value) {
if (value == null || value.isEmpty) {
return 'Enter your message to continue';
}
return null;
},
),
),
const SizedBox(width: 8),
StyledButton(
onPressed: () async {
if (_formKey.currentState!.validate()) {
await widget.addMessage(_controller.text);
_controller.clear();
}
},
child: Row(
children: const [
Icon(Icons.send),
SizedBox(width: 4),
Text('SEND'),
],
),
),
],
),
),
);
}
}
Aquí hay algunos puntos de interés. Primero, creas una instancia de un formulario para poder validar que el mensaje realmente contenga contenido y mostrarle al usuario un mensaje de error si no hay contenido. Para validar un formulario, accedes al estado del formulario detrás del formulario con un GlobalKey
. Para obtener más información sobre las claves y cómo usarlas, consulta Cuándo usar claves.
También observa la forma en que se disponen los widgets: tienes un Row
con un TextFormField
y un StyledButton
, que contiene un Row
. También observa que el elemento TextFormField
está unido en un widget Expanded
, lo que obliga al elemento TextFormField
a llenar cualquier espacio adicional en la fila. Para comprender mejor por qué se requiere, consulta Información sobre las restricciones.
Ahora que tienes un widget que permite al usuario ingresar texto para agregarlo al libro de visitas, debes mostrarlo en la pantalla.
- Edita el cuerpo de
HomePage
para agregar las siguientes dos líneas al final de los elementos secundarios deListView
:
const Header("What we'll be doing"),
const Paragraph(
'Join us for a day full of Firebase Workshops and Pizza!',
),
// Add the following two lines.
const Header('Discussion'),
GuestBook(addMessage: (message) => print(message)),
Si bien esto es suficiente para mostrar el widget, no es suficiente para hacer nada útil. Actualizarás este código en breve para que sea funcional.
Vista previa de la app
Cuando un usuario hace clic en ENVIAR, se activa el siguiente fragmento de código. Agrega el contenido del campo de entrada de mensaje a la colección guestbook
de la base de datos. Específicamente, el método addMessageToGuestBook
agrega el contenido del mensaje a un documento nuevo con un ID generado automáticamente en la colección guestbook
.
Ten en cuenta que FirebaseAuth.instance.currentUser.uid
es una referencia al ID único generado automáticamente que Authentication proporciona para todos los usuarios que accedieron.
- En el archivo
lib/app_state.dart
, agrega el métodoaddMessageToGuestBook
. Conectarás esta capacidad con la interfaz de usuario en el siguiente paso.
lib/app_state.dart
import 'package:cloud_firestore/cloud_firestore.dart'; // new
import 'package:firebase_auth/firebase_auth.dart'
hide EmailAuthProvider, PhoneAuthProvider;
import 'package:firebase_core/firebase_core.dart';
import 'package:firebase_ui_auth/firebase_ui_auth.dart';
import 'package:flutter/material.dart';
import 'firebase_options.dart';
class ApplicationState extends ChangeNotifier {
// Current content of ApplicationState elided ...
// Add from here...
Future<DocumentReference> addMessageToGuestBook(String message) {
if (!_loggedIn) {
throw Exception('Must be logged in');
}
return FirebaseFirestore.instance
.collection('guestbook')
.add(<String, dynamic>{
'text': message,
'timestamp': DateTime.now().millisecondsSinceEpoch,
'name': FirebaseAuth.instance.currentUser!.displayName,
'userId': FirebaseAuth.instance.currentUser!.uid,
});
}
// ...to here.
}
Conecta la IU y la base de datos
Tienes una IU en la que el usuario puede ingresar el texto que quiere agregar al libro de visitas y el código para agregar la entrada a Firestore. Ahora, solo tienes que conectar los dos.
- En el archivo
lib/home_page.dart
, realiza el siguiente cambio en el widgetHomePage
:
lib/home_page.dart
import 'package:firebase_auth/firebase_auth.dart'
hide EmailAuthProvider, PhoneAuthProvider;
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
import 'app_state.dart';
import 'guest_book.dart'; // new
import 'src/authentication.dart';
import 'src/widgets.dart';
class HomePage extends StatelessWidget {
const HomePage({Key? key}) : super(key: key);
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text('Firebase Meetup'),
),
body: ListView(
children: <Widget>[
Image.asset('assets/codelab.png'),
const SizedBox(height: 8),
const IconAndDetail(Icons.calendar_today, 'October 30'),
const IconAndDetail(Icons.location_city, 'San Francisco'),
Consumer<ApplicationState>(
builder: (context, appState, _) => AuthFunc(
loggedIn: appState.loggedIn,
signOut: () {
FirebaseAuth.instance.signOut();
}),
),
const Divider(
height: 8,
thickness: 1,
indent: 8,
endIndent: 8,
color: Colors.grey,
),
const Header("What we'll be doing"),
const Paragraph(
'Join us for a day full of Firebase Workshops and Pizza!',
),
// Modify from here...
Consumer<ApplicationState>(
builder: (context, appState, _) => Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
if (appState.loggedIn) ...[
const Header('Discussion'),
GuestBook(
addMessage: (message) =>
appState.addMessageToGuestBook(message),
),
],
],
),
),
// ...to here.
],
),
);
}
}
Reemplazaste las dos líneas que agregaste al comienzo de este paso por la implementación completa. Vuelves a usar Consumer<ApplicationState>
para que el estado de la app esté disponible para la parte del árbol que renderizas. Esto te permite reaccionar ante alguien que ingresa un mensaje en la IU y publicarlo en la base de datos. En la siguiente sección, probarás si los mensajes agregados se publican en la base de datos.
Pruebe enviar mensajes
- Si es necesario, accede a la app.
- Ingresa un mensaje, como
Hey there!
, y, luego, haz clic en ENVIAR.
Esta acción escribe el mensaje en tu base de datos de Firestore. Sin embargo, no verás el mensaje en tu app de Flutter real porque aún debes implementar la recuperación de los datos, lo que harás en el siguiente paso. Sin embargo, en el panel de Database de Firebase console, puedes ver el mensaje que agregaste en la colección guestbook
. Si envías más mensajes, agregarás más documentos a tu colección guestbook
. Por ejemplo, consulta el siguiente fragmento de código:
7. Lea los mensajes
Es genial que los invitados puedan escribir mensajes en la base de datos, pero aún no pueden verlos en la app. Es hora de corregirlo.
Sincroniza mensajes
Para mostrar mensajes, debes agregar objetos de escucha que se activen cuando cambien los datos y, luego, crear un elemento de la IU que muestre los mensajes nuevos. Agregarás código al estado de la app que detecte los mensajes recién agregados de la app.
- Crea un archivo nuevo
guest_book_message.dart
y agrega la siguiente clase para exponer una vista estructurada de los datos que almacenas en Firestore.
lib/guest_book_message.dart
class GuestBookMessage {
GuestBookMessage({required this.name, required this.message});
final String name;
final String message;
}
- En el archivo
lib/app_state.dart
, agrega las siguientes importaciones:
lib/app_state.dart
import 'dart:async'; // new
import 'package:cloud_firestore/cloud_firestore.dart';
import 'package:firebase_auth/firebase_auth.dart'
hide EmailAuthProvider, PhoneAuthProvider;
import 'package:firebase_core/firebase_core.dart';
import 'package:firebase_ui_auth/firebase_ui_auth.dart';
import 'package:flutter/material.dart';
import 'firebase_options.dart';
import 'guest_book_message.dart'; // new
- En la sección de
ApplicationState
donde defines el estado y los getters, agrega las siguientes líneas:
lib/app_state.dart
bool _loggedIn = false;
bool get loggedIn => _loggedIn;
// Add from here...
StreamSubscription<QuerySnapshot>? _guestBookSubscription;
List<GuestBookMessage> _guestBookMessages = [];
List<GuestBookMessage> get guestBookMessages => _guestBookMessages;
// ...to here.
- En la sección de inicialización de
ApplicationState
, agrega las siguientes líneas para suscribirte a una consulta sobre la colección de documentos cuando un usuario accede y anular la suscripción cuando sale:
lib/app_state.dart
Future<void> init() async {
await Firebase.initializeApp(
options: DefaultFirebaseOptions.currentPlatform);
FirebaseUIAuth.configureProviders([
EmailAuthProvider(),
]);
FirebaseAuth.instance.userChanges().listen((user) {
if (user != null) {
_loggedIn = true;
_guestBookSubscription = FirebaseFirestore.instance
.collection('guestbook')
.orderBy('timestamp', descending: true)
.snapshots()
.listen((snapshot) {
_guestBookMessages = [];
for (final document in snapshot.docs) {
_guestBookMessages.add(
GuestBookMessage(
name: document.data()['name'] as String,
message: document.data()['text'] as String,
),
);
}
notifyListeners();
});
} else {
_loggedIn = false;
_guestBookMessages = [];
_guestBookSubscription?.cancel();
}
notifyListeners();
});
}
Esta sección es importante porque es donde construyes una consulta sobre la colección guestbook
y controlas la suscripción y la cancelación de la suscripción a esta colección. Escuchas la transmisión, en la que reconstruyes una caché local de los mensajes en la colección guestbook
y también almacenas una referencia a esta suscripción para que puedas cancelar la suscripción más adelante. Aquí suceden muchas cosas, por lo que deberías explorarlo en un depurador para inspeccionar lo que sucede y obtener un modelo mental más claro. Para obtener más información, consulta Obtén actualizaciones en tiempo real con Firestore.
- En el archivo
lib/guest_book.dart
, agrega la siguiente importación:
import 'guest_book_message.dart';
- En el widget
GuestBook
, agrega una lista de mensajes como parte de la configuración para conectar este estado cambiante a la interfaz de usuario:
lib/guest_book.dart
class GuestBook extends StatefulWidget {
// Modify the following line:
const GuestBook({
super.key,
required this.addMessage,
required this.messages,
});
final FutureOr<void> Function(String message) addMessage;
final List<GuestBookMessage> messages; // new
@override
_GuestBookState createState() => _GuestBookState();
}
- En
_GuestBookState
, modifica el métodobuild
de la siguiente manera para exponer esta configuración:
lib/guest_book.dart
class _GuestBookState extends State<GuestBook> {
final _formKey = GlobalKey<FormState>(debugLabel: '_GuestBookState');
final _controller = TextEditingController();
@override
// Modify from here...
Widget build(BuildContext context) {
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
// ...to here.
Padding(
padding: const EdgeInsets.all(8.0),
child: Form(
key: _formKey,
child: Row(
children: [
Expanded(
child: TextFormField(
controller: _controller,
decoration: const InputDecoration(
hintText: 'Leave a message',
),
validator: (value) {
if (value == null || value.isEmpty) {
return 'Enter your message to continue';
}
return null;
},
),
),
const SizedBox(width: 8),
StyledButton(
onPressed: () async {
if (_formKey.currentState!.validate()) {
await widget.addMessage(_controller.text);
_controller.clear();
}
},
child: Row(
children: const [
Icon(Icons.send),
SizedBox(width: 4),
Text('SEND'),
],
),
),
],
),
),
),
// Modify from here...
const SizedBox(height: 8),
for (var message in widget.messages)
Paragraph('${message.name}: ${message.message}'),
const SizedBox(height: 8),
],
// ...to here.
);
}
}
Encapsulas el contenido anterior del método build()
con un widget Column
y, luego, agregas una colección para al final de los elementos secundarios de Column
para generar un nuevo Paragraph
para cada mensaje de la lista de mensajes.
- Actualiza el cuerpo de
HomePage
para construir correctamenteGuestBook
con el nuevo parámetromessages
:
lib/home_page.dart
Consumer<ApplicationState>(
builder: (context, appState, _) => Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
if (appState.loggedIn) ...[
const Header('Discussion'),
GuestBook(
addMessage: (message) =>
appState.addMessageToGuestBook(message),
messages: appState.guestBookMessages, // new
),
],
],
),
),
Prueba la sincronización de mensajes
Firestore sincroniza los datos de forma automática e instantánea con los clientes suscritos a la base de datos.
Prueba la sincronización de mensajes:
- En la app, busca los mensajes que creaste antes en la base de datos.
- Escribir mensajes nuevos Aparecen de inmediato.
- Abre tu espacio de trabajo en varias ventanas o pestañas. Los mensajes se sincronizan en tiempo real en todas las ventanas y pestañas.
- Opcional: En el menú Database de Firebase console, borra, modifica o agrega mensajes nuevos de forma manual. Todos los cambios aparecen en la IU.
¡Felicitaciones! Leíste documentos de Firestore en tu app.
Vista previa de la app
8. Configura reglas de seguridad básicas
Inicialmente, configuraste Firestore para que use el modo de prueba, lo que significa que tu base de datos está abierta para lecturas y escrituras. Sin embargo, solo debes usar el modo de prueba durante las primeras etapas del desarrollo. Como práctica recomendada, debes configurar reglas de seguridad para tu base de datos a medida que desarrollas tu app, ya que la seguridad es fundamental para la estructura y el comportamiento de la app.
Las reglas de seguridad de Firebase te permiten controlar el acceso a los documentos y las colecciones de tu base de datos. La sintaxis de reglas flexibles te permite crear reglas que coincidan con todo, desde todas las operaciones de escritura en la base de datos hasta las operaciones en un documento específico.
Configura reglas de seguridad básicas:
- En el menú Develop de Firebase console, haz clic en Database > Rules. Deberías ver las siguientes reglas de seguridad predeterminadas y una advertencia sobre que las reglas son públicas:
- Identifica las colecciones en las que la app escribe datos:
En match /databases/{database}/documents
, identifica la colección que deseas proteger:
rules_version = '2';
service cloud.firestore {
match /databases/{database}/documents {
match /guestbook/{entry} {
// You'll add rules here in the next step.
}
}
Como usaste el UID de Authentication como un campo en cada documento del libro de visitas, puedes obtener el UID de Authentication y verificar que cualquier persona que intente escribir en el documento tenga un UID de Authentication coincidente.
- Agrega las reglas de lectura y escritura a tu conjunto de reglas:
rules_version = '2';
service cloud.firestore {
match /databases/{database}/documents {
match /guestbook/{entry} {
allow read: if request.auth.uid != null;
allow write:
if request.auth.uid == request.resource.data.userId;
}
}
}
Ahora, solo los usuarios que accedan a su cuenta podrán leer los mensajes en el libro de visitas, pero solo el autor de un mensaje podrá editarlo.
- Agrega validación de datos para garantizar que todos los campos esperados estén presentes en el documento:
rules_version = '2';
service cloud.firestore {
match /databases/{database}/documents {
match /guestbook/{entry} {
allow read: if request.auth.uid != null;
allow write:
if request.auth.uid == request.resource.data.userId
&& "name" in request.resource.data
&& "text" in request.resource.data
&& "timestamp" in request.resource.data;
}
}
}
9. Paso adicional: Practica lo que aprendiste
Cómo registrar el estado de confirmación de asistencia de un asistente
Por el momento, tu app solo permite que las personas chateen cuando están interesadas en el evento. Además, la única forma de saber si alguien viene es cuando lo dice en el chat.
En este paso, te organizarás y les informarás a las personas cuántas asistirán. Agregarás algunas capacidades al estado de la app. La primera es la capacidad de un usuario que accedió de indicar si asistirá. El segundo es un contador de cuántas personas asistirán.
- En el archivo
lib/app_state.dart
, agrega las siguientes líneas a la sección de descriptores de acceso deApplicationState
para que el código de la IU pueda interactuar con este estado:
lib/app_state.dart
int _attendees = 0;
int get attendees => _attendees;
Attending _attending = Attending.unknown;
StreamSubscription<DocumentSnapshot>? _attendingSubscription;
Attending get attending => _attending;
set attending(Attending attending) {
final userDoc = FirebaseFirestore.instance
.collection('attendees')
.doc(FirebaseAuth.instance.currentUser!.uid);
if (attending == Attending.yes) {
userDoc.set(<String, dynamic>{'attending': true});
} else {
userDoc.set(<String, dynamic>{'attending': false});
}
}
- Actualiza el método
init()
deApplicationState
de la siguiente manera:
lib/app_state.dart
Future<void> init() async {
await Firebase.initializeApp(
options: DefaultFirebaseOptions.currentPlatform);
FirebaseUIAuth.configureProviders([
EmailAuthProvider(),
]);
// Add from here...
FirebaseFirestore.instance
.collection('attendees')
.where('attending', isEqualTo: true)
.snapshots()
.listen((snapshot) {
_attendees = snapshot.docs.length;
notifyListeners();
});
// ...to here.
FirebaseAuth.instance.userChanges().listen((user) {
if (user != null) {
_loggedIn = true;
_emailVerified = user.emailVerified;
_guestBookSubscription = FirebaseFirestore.instance
.collection('guestbook')
.orderBy('timestamp', descending: true)
.snapshots()
.listen((snapshot) {
_guestBookMessages = [];
for (final document in snapshot.docs) {
_guestBookMessages.add(
GuestBookMessage(
name: document.data()['name'] as String,
message: document.data()['text'] as String,
),
);
}
notifyListeners();
});
// Add from here...
_attendingSubscription = FirebaseFirestore.instance
.collection('attendees')
.doc(user.uid)
.snapshots()
.listen((snapshot) {
if (snapshot.data() != null) {
if (snapshot.data()!['attending'] as bool) {
_attending = Attending.yes;
} else {
_attending = Attending.no;
}
} else {
_attending = Attending.unknown;
}
notifyListeners();
});
// ...to here.
} else {
_loggedIn = false;
_emailVerified = false;
_guestBookMessages = [];
_guestBookSubscription?.cancel();
_attendingSubscription?.cancel(); // new
}
notifyListeners();
});
}
Este código agrega una consulta siempre suscrita para determinar la cantidad de asistentes y una segunda consulta que solo está activa mientras un usuario accede para determinar si el usuario asistirá.
- Agrega la siguiente enumeración en la parte superior del archivo
lib/app_state.dart
.
lib/app_state.dart
enum Attending { yes, no, unknown }
- Crea un archivo nuevo
yes_no_selection.dart
y define un widget nuevo que actúe como botones de selección:
lib/yes_no_selection.dart
import 'package:flutter/material.dart';
import 'app_state.dart';
import 'src/widgets.dart';
class YesNoSelection extends StatelessWidget {
const YesNoSelection(
{super.key, required this.state, required this.onSelection});
final Attending state;
final void Function(Attending selection) onSelection;
@override
Widget build(BuildContext context) {
switch (state) {
case Attending.yes:
return Padding(
padding: const EdgeInsets.all(8.0),
child: Row(
children: [
FilledButton(
onPressed: () => onSelection(Attending.yes),
child: const Text('YES'),
),
const SizedBox(width: 8),
TextButton(
onPressed: () => onSelection(Attending.no),
child: const Text('NO'),
),
],
),
);
case Attending.no:
return Padding(
padding: const EdgeInsets.all(8.0),
child: Row(
children: [
TextButton(
onPressed: () => onSelection(Attending.yes),
child: const Text('YES'),
),
const SizedBox(width: 8),
FilledButton(
onPressed: () => onSelection(Attending.no),
child: const Text('NO'),
),
],
),
);
default:
return Padding(
padding: const EdgeInsets.all(8.0),
child: Row(
children: [
StyledButton(
onPressed: () => onSelection(Attending.yes),
child: const Text('YES'),
),
const SizedBox(width: 8),
StyledButton(
onPressed: () => onSelection(Attending.no),
child: const Text('NO'),
),
],
),
);
}
}
}
Comienza en un estado indeterminado, sin seleccionar Sí ni No. Una vez que el usuario selecciona si asistirá, muestra esa opción destacada con un botón relleno y la otra opción se muestra con una renderización plana.
- Actualiza el método
build()
deHomePage
para aprovecharYesNoSelection
, permitir que un usuario que accedió nomine si asistirá y mostrar la cantidad de asistentes al evento:
lib/home_page.dart
Consumer<ApplicationState>(
builder: (context, appState, _) => Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
// Add from here...
switch (appState.attendees) {
1 => const Paragraph('1 person going'),
>= 2 => Paragraph('${appState.attendees} people going'),
_ => const Paragraph('No one going'),
},
// ...to here.
if (appState.loggedIn) ...[
// Add from here...
YesNoSelection(
state: appState.attending,
onSelection: (attending) => appState.attending = attending,
),
// ...to here.
const Header('Discussion'),
GuestBook(
addMessage: (message) =>
appState.addMessageToGuestBook(message),
messages: appState.guestBookMessages,
),
],
],
),
),
Agregar reglas
Ya configuraste algunas reglas, por lo que se rechazarán los datos que agregues con los botones. Debes actualizar las reglas para permitir que se agreguen elementos a la colección attendees
.
- En la colección
attendees
, toma el UID de Authentication que usaste como nombre del documento y verifica que eluid
del remitente sea el mismo que el documento que está escribiendo:
rules_version = '2';
service cloud.firestore {
match /databases/{database}/documents {
// ... //
match /attendees/{userId} {
allow read: if true;
allow write: if request.auth.uid == userId;
}
}
}
Esto permite que todos lean la lista de asistentes, ya que no hay datos privados allí, pero solo el creador puede actualizarla.
- Agrega validación de datos para garantizar que todos los campos esperados estén presentes en el documento:
rules_version = '2';
service cloud.firestore {
match /databases/{database}/documents {
// ... //
match /attendees/{userId} {
allow read: if true;
allow write: if request.auth.uid == userId
&& "attending" in request.resource.data;
}
}
}
- Opcional: En la app, haz clic en los botones para ver los resultados en el panel de Firestore en Firebase console.
Vista previa de la app
10. ¡Felicitaciones!
Usaste Firebase para compilar una app web interactiva en tiempo real.