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:

Diagrama de flujo
Img 1: El diagrama de flujo de la aplicació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 la Pantalla 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 desde AuthService para el registro del usuario.
  • Si tiene éxito, emite AuthenticationSuccessState con los datos del usuario; de lo contrario, emite AuthenticationFailureState.
  • 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 desde AuthService 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 y contraseñ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 el Bloc 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. El Bloc 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 de authService 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 de authService 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.

ezgif.com-video-to-gif--1-
Img 2: Una pantalla de inicio de sesión que muestra un proceso de inicio de sesión con 2 de los 3 estados.

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 un CircularProgressIndicator.
  • Si el estado es AuthenticationFailureState, muestra un AlertDialog 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 el AuthenticationBloc 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

Your email address will not be published. Required fields are marked *