Cómo construir un flujo de autenticación de usuario seguro en Flutter con Firebase y la gestión de estado con Bloc
La autenticación de usuarios es fundamental para el desarrollo de aplicaciones móviles. Ayuda a garantizar que solo usuarios autorizados puedan acceder a información confidencial y realizar acciones dentro de una aplicación. En este tutorial, exploraremos cómo construir una autenticación segura de usuarios en Flutter utilizando Firebase para la autenticación y el patrón de gestión de estado Bloc.
La autenticación de usuario es fundamental para el desarrollo de aplicaciones móviles. Ayuda a garantizar que solo los usuarios autorizados puedan acceder a información confidencial y realizar acciones dentro de una aplicación.
En este tutorial, exploraremos cómo construir una autenticación segura de usuario en Flutter utilizando Firebase para la autenticación y el patrón de gestión de estado Bloc para manejar el estado de la aplicación. Al final, tendrás una sólida comprensión de cómo integrar la autenticación de Firebase e implementar un proceso de inicio de sesión y registro seguro utilizando Bloc.
Requisitos previos:
Para aprovechar al máximo este tutorial, debes tener lo siguiente:
- Un buen conocimiento de Flutter y Dart
- Una cuenta de Firebase: Crea una cuenta de Firebase si no tienes una. Puedes configurar un proyecto de Firebase a través de la Consola de Firebase.
Cómo funciona la autenticación de Firebase
La autenticación de Firebase es un servicio potente que simplifica el proceso de autenticación de usuarios en tu aplicación. Admite diversos métodos de autenticación, incluyendo correo electrónico/contraseña, redes sociales y más.
Una de las principales ventajas de la autenticación de Firebase es sus funciones de seguridad integradas, como el almacenamiento seguro de las credenciales de usuario y el cifrado de datos sensibles.
Descripción del diagrama de flujo
Visualicemos el flujo de acciones utilizando un diagrama de flujo para comprender el concepto que vas a aprender. Echa un vistazo al diagrama a continuación para tener una mejor comprensión:
La imagen anterior es un diagrama de flujo para visualizar el flujo de la aplicación, discutamos qué representa cada parte. Los rectángulos redondeados representan los puntos de inicio y finalización del flujo; los rectángulos morados representan las pantallas; los rectángulos azul claro representan los procesos que tienen lugar; y finalmente, el rombo representa la toma de decisiones.
- La aplicación comienza en la
Pantalla de Flujo de Autenticación
. - El
StreamBuilder
escucha los cambios en el estado de autenticación. - Si un usuario está autenticado, se dirige a la
Pantalla de Inicio
; de lo contrario, se dirige a laPantalla de Registro
. AuthenticationBloc
gestiona los eventos y estados de autenticación del usuario.- Cuando el usuario se registra (
SignUpUser
se activa el evento): - Inicia el estado de carga de autenticación (
AuthenticationLoadingState
). - Llama a
signUpUser
desdeAuthService
para el registro del usuario. - Si tiene éxito, emite
AuthenticationSuccessState
con los datos del usuario; de lo contrario, emiteAuthenticationFailureState
. - Cuando el usuario inicia el proceso de cierre de sesión (
SignOut
se activa el evento): - Inicia el estado de carga de autenticación (
AuthenticationLoadingState
). - Llama a
signOutUser
desdeAuthService
para cerrar sesión del usuario. - Si ocurre un error durante el cierre de sesión, registra el mensaje de error.
Configuración del proyecto
Para comenzar con la autenticación de Firebase, debes configurar Firebase en tu proyecto de Flutter.
Sigue estos pasos para agregar Firebase y bloc a tu proyecto:
Agregar dependencias a tu proyecto
Abre tu proyecto en tu editor de código preferido.
Agrega las siguientes dependencias a tu archivo pubspec.yaml
:
dependencies:firebase_core: ^2.20.0firebase_auth: ^4.12.0flutter_bloc: ^8.1.3
Luego guarda el archivo pubspec.yaml
para descargar las dependencias.
Configurar Firebase
Crea un nuevo proyecto de Firebase a través de la Consola de Firebase. Haz clic en autenticación en el proyecto y sigue las instrucciones proporcionadas.
Para obtener más información, puedes visitar el sitio web de Firebase.
Inicializar Firebase
Primero, abre el archivo main.dart
en la carpeta lib
.
Agrega el siguiente código al archivo para inicializar Firebase:
void main() async {WidgetsFlutterBinding.ensureInitialized();await Firebase.initializeApp( options: DefaultFirebaseOptions.currentPlatform);
El código anterior muestra el código para ejecutar la aplicación. No hay nada inusual en este código, excepto que hemos agregado código al void main
para inicializar Firebase.
El modelo de usuario
Antes de crear la clase Firebase para comunicarse con el servicio de Firebase, vamos a definir un UserModel para representar los datos del usuario.
Empieza creando un archivo user.dart
en el directorio lib
de tu proyecto.
Luego agrega el siguiente código en el archivo:
class UserModel {final String? id;final String? email;final String? displayName;UserModel({ this.id, this.email, this.displayName, });}
Ahora que has configurado Firebase y creado un modelo de usuario, necesitas crear una clase de servicio para comunicarte directamente con Firebase.
El servicio de autenticación
Crea una carpeta llamada services
, crea un archivo en esta carpeta llamado authentication.dart
. Ahora puedes agregar este código al archivo.
import 'package:firebase_auth/firebase_auth.dart';import '../models/user.dart';class AuthService { final FirebaseAuth _firebaseAuth = FirebaseAuth.instance; /// crea usuario Future<UserModel?> signUpUser( String email, String password, ) async { try { final UserCredential userCredential = await _firebaseAuth.createUserWithEmailAndPassword( email: email.trim(), password: password.trim(), ); final User? firebaseUser = userCredential.user; if (firebaseUser != null) { return UserModel( id: firebaseUser.uid, email: firebaseUser.email ?? '', displayName: firebaseUser.displayName ?? '', ); } } on FirebaseAuthException catch (e) { print(e.toString()); } return null; } ///cerrar sesión Future<void> signOutUser() async { final User? firebaseUser = FirebaseAuth.instance.currentUser; if (firebaseUser != null) { await FirebaseAuth.instance.signOut(); } } // ... (otros métodos)}}
El fragmento de código anterior es un método para crear un usuario en la aplicación utilizando Firebase. Con este método, el método signUpUser
toma dos parámetros de tipo cadena: email
y password
respectivamente. Luego llamas al método de Firebase para crear un usuario utilizando los parámetros que hemos agregado.
Ahora que sabes cómo crear el método de registro, también puedes crear el método de inicio de sesión. La clase finalmente representa la comunicación entre Firebase y la aplicación.
La siguiente parte es conectar el servicio con tu gestor de estado, lo cual veremos cómo hacer ahora.
Cómo funciona el gestor de estado Bloc
Bloc es un patrón popular de gestión de estado para Flutter que ayuda a manejar estados de aplicación complejos de manera predecible y de forma testable. Bloc significa “Business Logic Component” y divide la lógica de negocio y la interfaz de usuario. Bloc será el puente entre tu aplicación y Firebase.
Hay una extensión para VScode que crea el código de inicio básico para Bloc. Puedes utilizar la extensión para acelerar el proceso de desarrollo.
Configurar el Bloc de autenticación de Firebase
Bloc consta de eventos y estados. Primero crearemos los estados y eventos para el Bloc. Luego crearemos un AuthenticationBloc
que manejará la lógica utilizando los eventos, estados y servicio que hemos creado.
La clase AuthenticationState
La clase AuthenticationState
es responsable de los diferentes estados del proceso de autenticación. Como veremos en el código, hay estados iniciales, de carga, de éxito y de fallo para asegurarnos de saber qué sucede durante el proceso de autenticación.
Primero, crea un archivo authentication_state.dart
en el directorio bloc
de tu proyecto.
parte de 'authentication_bloc.dart';
clase abstracta AuthenticationState {
const AuthenticationState();
List<Object> get props => [];
}
clase AuthenticationInitialState extiends AuthenticationState {}
clase AuthenticationLoadingState extiende AuthenticationState {
final bool isLoading;
AuthenticationLoadingState({requerido this.isLoading});
}
clase AuthenticationSuccessState extiende AuthenticationState {
final UserModel usuario;
const AuthenticationSuccessState(this.user);
@override
List<Object> get props => [user];
}
clase AuthenticationFailureState extiende AuthenticationState {
final String mensajeError;
const AuthenticationFailureState(this.mensajeError);
@override
List<Object> get props => [mensajeError];
}
Desglosemos el código:
Clase abstracta AuthenticationState
:
AuthenticationState
es la clase base para los diferentes estados en los que puede estar el proceso de autenticación.- Contiene un método
props
que devuelve una lista de objetos. Este método se utiliza para comparar instancias de esta clase.
Clase AuthenticationInitialState
:
AuthenticationInitialState
representa el estado inicial del proceso de autenticación.
Clase AuthenticationLoadingState
:
AuthenticationLoadingState
representa un estado en el que el proceso de autenticación está en progreso y la interfaz de usuario puede mostrar un indicador de carga.- Recibe un parámetro booleano,
isLoading
, para indicar si el proceso de autenticación está cargando actualmente o no.
Clase AuthenticationSuccessState
:
AuthenticationSuccessState
representa un estado en el que el proceso de autenticación se ha completado.- Incluye una propiedad de usuario de tipo UserModel que representa al usuario autenticado.
Clase AuthenticationFailureState
:
AuthenticationFailureState
representa un estado en el que el proceso de autenticación ha fallado.- Incluye una propiedad
mensajeError
que contiene información sobre el error.
La clase AuthenticationEvent
La clase AuthenticationEvent
es responsable de los eventos que realizará el AuthenticationBloc
. En este caso, se trata del evento de inicio de sesión. Puedes escribir los otros eventos, como registro y cierre de sesión, aquí.
Crea un archivo authentication_event.dart
en el directorio bloc
de tu proyecto.
parte de 'authentication_bloc.dart';
clase abstracta AuthenticationEvent {
const AuthenticationEvent();
List<Object> get props => [];
}
clase SignUpUser extiende AuthenticationEvent {
final String correoElectronico;
final String contraseña;
const SignUpUser(this.correoElectronico, this.contraseña);
@override
List<Object> get props => [correoElectronico, contraseña];
}
clase SignOut extiende AuthenticationEvent {}
La clase AuthenticationEvent
es similar a AuthenticationState
. Veamos el código para entender lo que hace:
Clase abstracta AuthenticationEvent
:
- Esta es la clase base para los diferentes eventos que desencadenan cambios en el estado de autenticación.
Clase SignUpUser
:
- Esta clase representa un evento en el que un usuario intenta registrarse.
- Recibe dos parámetros,
correoElectronico
ycontraseña
, que representan las credenciales que el usuario está utilizando para registrarse. - Las instancias de esta clase indicarán al
Bloc
que un usuario está intentando registrarse, y elBloc
puede responder iniciando el proceso de registro y transitando el estado de autenticación en consecuencia.
Clase SignOut
:
- Las instancias de esta clase indicarán al
Bloc
que un usuario está intentando cerrar sesión. ElBloc
puede responder iniciando el proceso de cierre de sesión y actualizando el estado de autenticación en consecuencia.
La clase AuthenticationBloc
El AuthenticationBloc
manejará el estado de autenticación en general, desde lo que sucede cuando un usuario hace clic en un botón hasta lo que se muestra en la pantalla. También interactúa directamente con el servicio de Firebase que creamos.
Primero, crea un archivo llamado authentication_bloc.dart
en el directorio bloc
de tu proyecto.
Agrega el siguiente código para definir la clase AuthenticationBloc
:
import 'package:bloc/bloc.dart';import 'package:meta/meta.dart';import '../models/user.dart';import '../services/authentication.dart';part 'authentication_event.dart';part 'authentication_state.dart';class AuthenticationBloc extends Bloc<AuthenticationEvent, AuthenticationState> { final AuthService authService = AuthService(); AuthenticationBloc() : super(AuthenticationInitialState()) { on<AuthenticationEvent>((event, emit) {}); on<SignUpUser>((event, emit) async { emit(AuthenticationLoadingState(isLoading: true)); try { final UserModel? user = await authService.signUpUser(event.email, event.password); if (user != null) { emit(AuthenticationSuccessState(user)); } else { emit(const AuthenticationFailureState('create user failed')); } } catch (e) { print(e.toString()); } emit(AuthenticationLoadingState(isLoading: false)); }); on<SignOut>((event, emit) async { emit(AuthenticationLoadingState(isLoading: true)); try { authService.signOutUser(); } catch (e) { print('error'); print(e.toString()); } emit(AuthenticationLoadingState(isLoading: false)); });}}
En este fragmento de código, hemos creado una instancia de la clase AuthService
, que maneja las operaciones de autenticación del usuario, como el registro y cierre de sesión.
on<SignUpUser>((event, emit) async { ... }
define un controlador para el evento SignUpUser
. Cuando se activa este evento, el bloc
realiza los siguientes pasos:
- Emite un
AuthenticationLoadingState
para indicar que el proceso de autenticación está en curso. - Llama al método
signUpUser
deauthService
para intentar crear una cuenta de usuario con el correo electrónico y la contraseña proporcionados. - Si la creación de la cuenta de usuario es exitosa (es decir, el usuario no es nulo), emite un
AuthenticationSuccessState
con los datos del usuario. - Si la creación de la cuenta de usuario falla, emite un
AuthenticationFailureState
con un mensaje de error y registra el error. - Independientemente del éxito o el fracaso, emite otro
AuthenticationLoadingState
para señalar el final del proceso de autenticación.
on<SignOut>((event, emit) async { ... }
define un controlador para el evento SignOut
. Cuando se activa este evento, el bloc
realiza los siguientes pasos:
- Emite un
AuthenticationLoadingState
para indicar que el proceso de cierre de sesión está en curso. - Llama al método
signOutUser
deauthService
para cerrar la sesión del usuario. - Si ocurren errores durante el proceso de cierre de sesión, registra el error.
- Emite otro
AuthenticationLoadingState
para señalar el final del proceso de cierre de sesión.
El AuthenticationBloc
gestiona el estado del proceso de autenticación, incluidos los estados de carga, éxito y falla, según los eventos desencadenados por las acciones del usuario. El authService
es responsable de llevar a cabo las operaciones de autenticación reales. Con el Bloc configurado, podemos implementar el flujo de autenticación utilizando Bloc.
Cómo implementar el flujo de autenticación con Bloc
Para implementar el flujo de autenticación, crearás un widget Stateless dedicado para verificar si un usuario ha iniciado sesión para saber qué pantalla mostrar al usuario. La página mostrará diferentes pantallas según el estado de autenticación del usuario.
AuthenticationFlowScreen
:
Crea un nuevo archivo llamado authentication_page.dart
en el directorio screens
de tu proyecto.
import 'package:bloc_authentication_flow/screens/home.dart';import 'package:bloc_authentication_flow/screens/sign_up.dart';import 'package:firebase_auth/firebase_auth.dart';import 'package:flutter/material.dart';class AuthenticationFlowScreen extends StatelessWidget { const AuthenticationFlowScreen({super.key}); static String id = 'main screen'; @override Widget build(BuildContext context) { return Scaffold( body: StreamBuilder( stream: FirebaseAuth.instance.authStateChanges(), builder: (context, snapshot) { if (snapshot.hasData) { return const HomeScreen(); } else { return const SignupScreen(); } }, ), ); }}
En el código anterior, tienes un StatelessWidget
con un StreamBuilder
como hijo. El StreamBuilder
actúa como un juez, utilizando Firebase para verificar los cambios de estado y si un usuario ha iniciado sesión o no. Si un usuario ha iniciado sesión, los dirige a la pantalla de inicio, de lo contrario, se dirige a la pantalla de registro.
Cambia la ruta de inicio a AuthenticationFlowScreen
para permitir que la aplicación verifique antes de dirigirse a cualquier página.
home: const AuthenticationFlowScreen()
Pantalla de registro
Primero, crea un nuevo archivo llamado sign_up.dart
en el directorio screens
.
import 'package:bloc_authentication_flow/screens/home.dart';import 'package:flutter/material.dart';import 'package:flutter_bloc/flutter_bloc.dart';import '../bloc/authentication_bloc.dart';class SignupScreen extends StatefulWidget { static String id = 'login_screen'; const SignupScreen({ Key? key, }) : super(key: key); @override State<SignupScreen> createState() => _SignupScreenState();}class _SignupScreenState extends State<SignupScreen> { // Controladores de texto final emailController = TextEditingController(); final passwordController = TextEditingController(); @override void dispose() { emailController.dispose(); passwordController.dispose(); super.dispose(); } @override Widget build(BuildContext context) { return Scaffold( appBar: AppBar( title: const Text( 'Iniciar sesión en su cuenta', style: TextStyle( color: Colors.deepPurple, ), ), centerTitle: true, ), body: Padding( padding: const EdgeInsets.all(16.0), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ const SizedBox(height: 20), const Text('Dirección de correo electrónico'), const SizedBox(height: 10), TextFormField( controller: emailController, decoration: const InputDecoration( border: OutlineInputBorder(), hintText: 'Ingrese su correo electrónico', ), ), const SizedBox(height: 10), const Text('Contraseña'), TextFormField( controller: passwordController, decoration: const InputDecoration( border: OutlineInputBorder(), hintText: 'Ingrese su contraseña', ), obscureText: false, ), const SizedBox(height: 10), GestureDetector( onTap: () {}, child: const Text( '¿Olvidó su contraseña?', style: TextStyle( color: Colors.deepPurple, ), ), ), const SizedBox(height: 20), BlocConsumer<AuthenticationBloc, AuthenticationState>( listener: (context, state) { if (state is AuthenticationSuccessState) { Navigator.pushNamedAndRemoveUntil( context, HomeScreen.id, (route) => false, ); } else if (state is AuthenticationFailureState) { showDialog( context: context, builder: (context) { return const AlertDialog( content: Text('error'), ); }); } }, builder: (context, state) { return SizedBox( height: 50, width: double.infinity, child: ElevatedButton( onPressed: () { BlocProvider.of<AuthenticationBloc>(context).add( SignUpUser( emailController.text.trim(), passwordController.text.trim(), ), ); }, child: Text( state is AuthenticationLoadingState ? '.......', : 'Registrarse', style: TextStyle( fontSize: 20, ), ), ), ); }, ), const SizedBox(height: 20), Row( mainAxisAlignment: MainAxisAlignment.center, children: [ const Text("¿Ya tienes una cuenta? "), GestureDetector( onTap: () {}, child: const Text( 'Iniciar sesión', style: TextStyle( color: Colors.deepPurple, ), ), ) ], ), ], ), ), ); }}
Este código es solo una interfaz de inicio de sesión simple con dos campos de texto (textfields
) y un botón elevado. El widget BlocConsumer
envuelve el botón Registrarse
y escucha los cambios en el estado de AuthenticationBloc
. Cuando un usuario presiona el botón, envía un evento al AuthenticationBloc
para iniciar el proceso de registro de usuario.
Dependiendo del estado de autenticación, este botón puede mostrar diferentes respuestas o navegar a otra pantalla. Verifica los estados AuthenticationSuccessState
, AuthenticationLoadingState
y AuthenticationFailureState
para responder en consecuencia.
Pantalla de Inicio
Crea otro archivo llamado home_screen.dart
en el directorio screens
y agrega el siguiente código al archivo.
import 'package:flutter/material.dart';import 'package:flutter_bloc/flutter_bloc.dart';import '../bloc/authentication_bloc.dart';class HomeScreen extends StatelessWidget { static String id = 'home_screen'; const HomeScreen({super.key}); @override Widget build(BuildContext context) { return Scaffold( body: Center( child: Column( mainAxisAlignment: MainAxisAlignment.center, children: [ const Text( 'Hola Usuario', style: TextStyle( fontSize: 20, ), ), const SizedBox( height: 20, ), BlocConsumer<AuthenticationBloc, AuthenticationState>( listener: (context, state) { if (state is AuthenticationLoadingState) { const CircularProgressIndicator(); } else if (state is AuthenticationFailureState){ showDialog(context: context, builder: (context){ return const AlertDialog( content: Text('error'), ); }); } }, builder: (context, state) { return ElevatedButton( onPressed: () { BlocProvider.of<AuthenticationBloc>(context) .add(SignOut()); }, child: const Text( 'Cerrar Sesión' )); }, ), ], ), ), ); }}
El código anterior representa la Pantalla de Inicio
y también es una página simple que consta de un scaffold, una columna y un widget de texto, pero la parte interesante es el BlocConsumer
que se encuentra en el botón elevado que dice cerrar sesión. Vamos a observar eso de cerca.
El BlocConsumer
escucha los cambios de estado del AuthenticationBloc
. Tiene dos parámetros: listener y builder.
- listener: Escucha los cambios de estado y reacciona según el estado actual recibido del
AuthenticationBloc
. - Si el estado es
AuthenticationLoadingState
, muestra unCircularProgressIndicator
. - Si el estado es
AuthenticationFailureState
, muestra unAlertDialog
con el mensaje ‘Error’. - builder: Construye la interfaz de usuario según el estado actual recibido del
AuthenticationBloc
. - Renderiza un
ElevatedButton
con la etiqueta “Cerrar Sesión”. - Cuando se presiona, activa el evento
SignOut
en elAuthenticationBloc
a través de BlocProvider.
Con el flujo de autenticación Bloc implementado, puedes ejecutar tu aplicación Flutter y probar las funcionalidades de registro. Asegúrate también de manejar otros escenarios relacionados con la autenticación, como el inicio de sesión de usuarios y la recuperación de contraseñas, según lo requieran las especificaciones de tu aplicación. Además, es importante manejar los errores de manera adecuada para brindar una buena experiencia al usuario.
Si deseas clonar el repositorio, puedes hacerlo en GitHub aquí y dejar un “me gusta”.
Conclusión
En este artículo, exploramos cómo construir un flujo de autenticación de usuarios en Flutter utilizando Firebase para la autenticación y el patrón de gestión de estado Bloc para administrar el estado de la aplicación.
Aprendimos cómo configurar Firebase en un proyecto de Flutter, crear Blocs para la autenticación e implementar el flujo de autenticación utilizando Bloc.
Al aprovechar el poder de Firebase y la previsibilidad de Bloc, puedes garantizar una experiencia de autenticación de usuario segura y sin problemas en tus aplicaciones Flutter.
Leave a Reply