Flutter Firebase Push Notification Provider
Un paquete Flutter que proporciona una interfaz unificada y optimizada para gestionar notificaciones push de Firebase Cloud Messaging (FCM). Maneja permisos, tokens y mensajes en todos los estados de la aplicación (foreground, background y terminada) con gestión automática de memoria y prevención de leaks.
Features
- ✅ Manejo automático de notificaciones en foreground, background y cuando la app está cerrada
- ✅ Stream unificado para recibir todas las notificaciones
- ✅ Gestión automática de permisos de notificación
- ✅ Actualización automática de tokens FCM en el servidor MongoDB
- ✅ Filtrado de mensajes por
IdPushNotification - ✅ Prevención de memory leaks con gestión apropiada de subscriptions
- ✅ Protección contra inicialización múltiple
- ✅ Manejo robusto de errores con propagación apropiada
- ✅ Integración automática con información del dispositivo y versión de la app
- ✅ Debug logging integrado con formato consistente
- ✅ Verificación de tokens existentes para evitar duplicados
- ✅ Gestión de estados de registros (activo/bloqueado)
- ✅ Anulación de tokens con eliminación de FCM y actualización de estado
Getting started
Prerequisitos
- Flutter SDK >= 1.17.0
- Dart SDK >= 3.3.2
- Firebase configurado en tu proyecto (Guía de configuración)
- Servidor MongoDB configurado con la colección requerida (ver sección "Configuración del Servidor MongoDB")
- Paquetes adicionales requeridos (disponibles en pub.flutter-io.cn):
api_rest_flutter_webflutter_utils_providersflutter_models_provider
Instalación
Agrega el paquete a tu pubspec.yaml:
dependencies:
flutter_firebase_push_notification_provider_web: ^3.0.0
Luego ejecuta:
flutter pub get
Configuración del Servidor MongoDB
⚠️ REQUISITO FUNDAMENTAL
Antes de usar este paquete, es requisito fundamental que tu servidor MongoDB tenga configurada la siguiente colección según se define en EnvironmentPushNotification:
Nombre de la Colección
tokenFirebase
Estructura de la Colección MongoDB
La colección debe tener los siguientes campos en el orden especificado:
| Campo | Tipo | Descripción | Ejemplo |
|---|---|---|---|
data.idUsuario |
String | ID del usuario en la aplicación | "user_12345" |
data.token o data.tokenFirebase |
String | Token de Firebase Cloud Messaging | "dGh1c2lzYXRva2Vu..." |
data.dispositivo |
String | Información del dispositivo | "Samsung Galaxy S21" |
data.plataforma |
String | Plataforma del dispositivo | "android" o "ios" |
data.appVersion |
String | Versión de la aplicación | "1.0.0" |
estado |
String | Estado del registro | "activo" o "bloqueado" |
⚠️ Nota sobre nomenclatura: Actualmente existe una inconsistencia en el código donde:
- Al crear el registro se usa el campo
data.token(definido enEnvironmentPushNotification.camposColeccionTokenFirebase) - Al consultar el registro se usa el campo
data.tokenFirebase(en métodosanularToken()y_verificarTokenExistente())
Recomendación: Asegúrate de que tu colección MongoDB use data.tokenFirebase como nombre del campo, o actualiza el código para usar consistentemente data.token.
Ejemplo de Documento en MongoDB (Data)
{
"_id": "507f1f77bcf86cd799439011",
"idServer": "507f1f77bcf86cd799439011",
"idMobile": "mobile_123",
"estado": "activo",
"creadoEl": "2025-01-15T10:30:00Z",
"data": {
"idUsuario": "user_12345",
"tokenFirebase": "dGh1c2lzYXRva2VuZnJvbWZpcmViYXNl...",
"dispositivo": "Samsung Galaxy S21",
"plataforma": "android",
"appVersion": "1.0.0"
}
}
Nota: El ejemplo muestra el campo como tokenFirebase según la implementación actual en las consultas.
Índices Recomendados
Para optimizar el rendimiento, crea los siguientes índices en la colección:
// Índice único compuesto para evitar duplicados
db.tokenFirebase.createIndex(
{ "data.idUsuario": 1, "data.tokenFirebase": 1 },
{ unique: true }
)
// Índice para búsquedas por usuario
db.tokenFirebase.createIndex({ "data.idUsuario": 1 })
// Índice para búsquedas por token
db.tokenFirebase.createIndex({ "data.tokenFirebase": 1 })
// Índice para búsquedas por estado
db.tokenFirebase.createIndex({ "estado": 1 })
// Índice compuesto para consultas de tokens activos
db.tokenFirebase.createIndex(
{ "data.tokenFirebase": 1, "estado": 1 }
)
⚠️ IMPORTANTE: Orden de Campos
El orden de los campos en EnvironmentPushNotification.camposColeccionTokenFirebase es crítico:
static const camposColeccionTokenFirebase = [
'idUsuario', // 1. Primero
'token', // 2. Segundo
'dispositivo', // 3. Tercero
'plataforma', // 4. Cuarto
'appVersion', // 5. Quinto
];
Este orden debe coincidir con el orden en que se pasan los datos en el código. No alterar este orden.
Usage
1. Inicialización en main.dart
Importante: Ya no necesitas proporcionar coleccionFireBaseTokens porque ahora se usa la configuración de EnvironmentPushNotification.
import 'package:firebase_core/firebase_core.dart';
import 'package:flutter_firebase_push_notification_provider/flutter_firebase_push_notification_provider.dart';
import 'package:api_rest_flutter/api_rest.dart';
import 'package:api_rest_flutter/util/utils_data_api_rest.dart';
void main() async {
WidgetsFlutterBinding.ensureInitialized();
// Inicializar Firebase primero
await Firebase.initializeApp();
// Crear instancias necesarias
final apiRest = ApiRest(/* tu configuración */);
final utilsDataApiRest = UtilsDataApiRest(/* tu configuración */);
// Inicializar el manejador de notificaciones (usando el Singleton)
try {
final notificationProvider = FlutterFireBasePushNotificationProvider();
await notificationProvider.initializeApp(
apiRest: apiRest,
utilsDataApiRest: utilsDataApiRest,
idUsuario: 'USER_ID', // ID del usuario actual
coleccionAuth: 'auth', // Colección de autenticación
);
print('✅ Notificaciones inicializadas correctamente');
} catch (e) {
print('❌ Error al inicializar notificaciones: $e');
}
runApp(MyApp());
}
2. Escuchar mensajes en tu widget
import 'dart:async';
import 'package:flutter_firebase_push_notification_provider/flutter_firebase_push_notification_provider.dart';
class MyWidget extends StatefulWidget {
@override
_MyWidgetState createState() => _MyWidgetState();
}
class _MyWidgetState extends State<MyWidget> {
StreamSubscription<Map<String, dynamic>>? _subscription;
late final FlutterFireBasePushNotificationProvider _notificationProvider;
@override
void initState() {
super.initState();
// Obtener la instancia del Singleton (siempre devuelve la misma instancia)
_notificationProvider = FlutterFireBasePushNotificationProvider();
// Verificar que esté inicializado antes de suscribirse
if (_notificationProvider.isInitialized) {
_suscribirseANotificaciones();
} else {
print('⚠️ El provider no está inicializado');
}
}
void _suscribirseANotificaciones() {
// Escuchar mensajes del stream
_subscription = _notificationProvider.messagesStream.listen(
(mensaje) {
// Procesar notificación
final idNotification = mensaje['data']?['IdPushNotification'];
if (idNotification != null) {
print('📩 ID Notificación: $idNotification');
print('📩 Título: ${mensaje['notification']?['title'] ?? 'Sin título'}');
print('📩 Mensaje: ${mensaje['notification']?['body'] ?? 'Sin mensaje'}');
// Aquí puedes navegar a una pantalla específica o mostrar un dialog
_procesarNotificacion(mensaje);
}
},
onError: (error) {
print('❌ Error en el stream de notificaciones: $error');
},
);
}
void _procesarNotificacion(Map<String, dynamic> mensaje) {
// Implementa tu lógica de negocio aquí
// Por ejemplo: navegación, actualización de UI, etc.
}
@override
void dispose() {
// Cancelar la suscripción para evitar memory leaks
_subscription?.cancel();
super.dispose();
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: Text('Notificaciones')),
body: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Text('Estado: ${_notificationProvider.isInitialized ? "Inicializado" : "No inicializado"}'),
SizedBox(height: 16),
ElevatedButton(
onPressed: _notificationProvider.hasPermission
? () => print('✅ Permisos otorgados')
: () => print('⚠️ Sin permisos de notificación'),
child: Text('Verificar Permisos'),
),
],
),
),
);
}
}
3. Obtener el Token FCM
Existen dos formas de obtener el token FCM:
3.1. Acceder al token actual (Forma recomendada)
Puedes acceder directamente al token actual almacenado sin hacer una llamada asíncrona:
final notificationProvider = FlutterFireBasePushNotificationProvider();
// Acceso directo al token actual
final currentToken = notificationProvider.currentToken;
if (currentToken.isNotEmpty) {
print('Token FCM actual: $currentToken');
// Hacer algo con el token
} else {
print('No hay token disponible');
}
3.2. Obtener el token manualmente (Llamada asíncrona)
Si necesitas obtener el token directamente desde Firebase (por ejemplo, para forzar una actualización):
final notificationProvider = FlutterFireBasePushNotificationProvider();
final token = await notificationProvider.getToken();
if (token.isNotEmpty) {
print('Token FCM: $token');
// Hacer algo con el token
} else {
print('No se pudo obtener el token');
}
Nota: La propiedad currentToken es más eficiente ya que accede directamente al token almacenado sin necesidad de hacer una llamada asíncrona a Firebase.
4. Anular un Token FCM (Cerrar Sesión/Dispositivo)
Cuando un usuario cierra sesión o necesitas desactivar las notificaciones push en un dispositivo específico, debes anular el token de Firebase. Este método:
- Elimina el token de Firebase Cloud Messaging en el dispositivo
- Marca el token como bloqueado en tu base de datos MongoDB (cambia el estado de
activoabloqueado)
Importante: El método anularToken() usa automáticamente el token actual almacenado (currentToken), por lo que NO necesitas pasar el token como parámetro.
¿Cuándo usar anularToken()?
- 🔐 Cierre de sesión: Cuando el usuario hace logout
- 📱 Cambio de dispositivo: Cuando el usuario se muda a otro dispositivo
- 🔒 Seguridad: Cuando se detecta actividad sospechosa y necesitas revocar acceso
- 🚫 Desactivar notificaciones: Cuando el usuario desea dejar de recibir notificaciones
Ejemplo de Uso
import 'package:flutter_firebase_push_notification_provider/flutter_firebase_push_notification_provider.dart';
Future<void> cerrarSesionUsuario() async {
final notificationProvider = FlutterFireBasePushNotificationProvider();
try {
// Anular el token en Firebase y marcar como bloqueado en MongoDB
// El método usa automáticamente el token actual almacenado
await notificationProvider.anularToken(
apiRest: apiRest,
utilsDataApiRest: utilsDataApiRest,
);
print('✅ Token anulado correctamente');
print('📱 El dispositivo ya no recibirá notificaciones');
// Continuar con el proceso de cierre de sesión
// Navigator.pushReplacement(...);
} catch (e) {
print('❌ Error al anular token: $e');
// Manejar el error apropiadamente
// Podrías mostrar un dialog o continuar con el logout de todas formas
}
}
Ejemplo Completo en un Botón de Logout
ElevatedButton(
onPressed: () async {
// Mostrar diálogo de confirmación
final confirmar = await showDialog<bool>(
context: context,
builder: (context) => AlertDialog(
title: Text('Cerrar Sesión'),
content: Text('¿Estás seguro que deseas cerrar sesión?'),
actions: [
TextButton(
onPressed: () => Navigator.pop(context, false),
child: Text('Cancelar'),
),
TextButton(
onPressed: () => Navigator.pop(context, true),
child: Text('Sí, cerrar sesión'),
),
],
),
);
if (confirmar == true) {
// Mostrar indicador de carga
showDialog(
context: context,
barrierDismissible: false,
builder: (context) => Center(child: CircularProgressIndicator()),
);
try {
final notificationProvider = FlutterFireBasePushNotificationProvider();
// Anular token FCM (usa automáticamente el token actual almacenado)
await notificationProvider.anularToken(
apiRest: apiRest,
utilsDataApiRest: utilsDataApiRest,
);
// Limpiar sesión local, SharedPreferences, etc.
// await _limpiarSesionLocal();
// Cerrar indicador de carga
Navigator.pop(context);
// Navegar a pantalla de login
Navigator.pushReplacementNamed(context, '/login');
} catch (e) {
// Cerrar indicador de carga
Navigator.pop(context);
// Mostrar error
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text('Error al cerrar sesión: $e'),
backgroundColor: Colors.red,
),
);
}
}
},
child: Text('Cerrar Sesión'),
)
¿Qué hace anularToken() internamente?
- Verifica el token actual: Comprueba que
currentTokenno esté vacío - Elimina el token de FCM: Llama a
FirebaseMessaging.instance.deleteToken()para eliminar el token del dispositivo - Consulta MongoDB: Busca todos los registros en la colección
tokenFirebasedonde:tokenFirebase= token actual almacenado (_currentToken)estado="activo"
- Actualiza el estado: Cambia el estado de todos los registros encontrados a
"bloqueado" - Logs detallados: Registra cada operación para debugging
Gestión de Estados
El sistema utiliza dos estados para los registros de tokens:
| Estado | Descripción | Cuándo se usa |
|---|---|---|
activo |
Token válido y en uso | Cuando se crea o actualiza un token |
bloqueado |
Token desactivado | Cuando se anula un token con anularToken() |
Esto permite mantener un historial de tokens sin eliminarlos físicamente de la base de datos, lo cual es útil para:
- 📊 Auditoría: Saber qué dispositivos tenía el usuario
- 🔍 Debugging: Identificar problemas de tokens duplicados
- 📈 Análisis: Estadísticas de uso por dispositivo/plataforma
Manejo de Errores
try {
await notificationProvider.anularToken(
apiRest: apiRest,
utilsDataApiRest: utilsDataApiRest,
);
} on Exception catch (e) {
// Error específico de la operación
if (e.toString().contains('Error al leer registro')) {
print('⚠️ No se pudo consultar la base de datos');
} else if (e.toString().contains('Error al cambiar estado')) {
print('⚠️ No se pudo actualizar el estado del token');
} else {
print('❌ Error desconocido: $e');
}
// Decidir si continuar con logout de todas formas
// o mostrar error al usuario
} catch (e) {
print('❌ Error inesperado: $e');
}
Notas Importantes
- ⚠️ No reinicializar: Después de anular el token, NO intentes inicializar nuevamente sin reiniciar la app
- 🔄 Nuevo login: Si el usuario inicia sesión nuevamente, la app debe reiniciarse o el token debe regenerarse
- 📝 Logging: Todos los pasos se registran en logs de debug para facilitar troubleshooting
- 🗄️ MongoDB: Los registros NO se eliminan, solo se marcan como
bloqueado
5. Formato del mensaje
Los mensajes recibidos en el stream tienen el siguiente formato:
{
'senderId': String,
'category': String?,
'collapseKey': String?,
'contentAvailable': bool,
'data': Map<String, dynamic>, // Contiene IdPushNotification
'from': String?,
'messageId': String?,
'messageType': String?,
'mutableContent': bool,
'notification': {
'title': String?,
'body': String?,
'android': {...},
'apple': {...}
},
'sentTime': int?, // millisecondsSinceEpoch
'threadId': String?,
'ttl': int?
}
Nota importante: Solo se procesarán mensajes que incluyan el campo IdPushNotification en el objeto data.
6. Verificar estado y permisos
final notificationProvider = FlutterFireBasePushNotificationProvider();
// Verificar si está inicializado
if (notificationProvider.isInitialized) {
print('✅ Provider inicializado');
// Verificar permisos
if (notificationProvider.hasPermission) {
print('✅ Permisos de notificación otorgados');
} else {
print('⚠️ Sin permisos de notificación');
}
} else {
print('⚠️ Provider no inicializado');
}
// Verificar si está disposed
if (notificationProvider.isDisposed) {
print('⚠️ Provider ya fue disposed, necesita reinicio de la app');
}
7. Actualización automática de tokens
El paquete maneja automáticamente la actualización de tokens en tu servidor MongoDB cuando:
- Se obtiene el token inicial
- El token es actualizado por Firebase (token refresh)
Proceso de Actualización
- Verificación: El paquete primero verifica si el token ya existe en la base de datos
- Creación: Si no existe, crea un nuevo documento con toda la información
- Información Guardada:
idUsuario: ID del usuario actualtoken: Token FCMdispositivo: Marca y modelo del dispositivo (ej: "Samsung Galaxy S21")plataforma: "android" o "ios"appVersion: Versión de la app (ej: "1.0.0")
No necesitas llamar manualmente ningún método, todo se maneja automáticamente una vez inicializado.
8. Manejo del ciclo de vida de la app
class MyApp extends StatefulWidget {
@override
_MyAppState createState() => _MyAppState();
}
class _MyAppState extends State<MyApp> with WidgetsBindingObserver {
@override
void initState() {
super.initState();
WidgetsBinding.instance.addObserver(this);
}
@override
void dispose() {
WidgetsBinding.instance.removeObserver(this);
// SOLO disponer si la app se está cerrando completamente
// NO llamar en dispose normal de widgets
// final notificationProvider = FlutterFireBasePushNotificationProvider();
// notificationProvider.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Mi App',
home: HomeScreen(),
);
}
}
Manejo de Errores
El paquete propaga errores apropiadamente, lo que permite manejarlos en tu código:
try {
await notificationProvider.initializeApp(
apiRest: apiRest,
utilsDataApiRest: utilsDataApiRest,
idUsuario: userId,
coleccionAuth: 'auth',
);
} on StateError catch (e) {
// Error de estado (ya inicializado, disposed, Firebase no inicializado)
print('Error de estado: $e');
} on FirebaseException catch (e) {
// Error de Firebase
print('Error de Firebase: ${e.message}');
} catch (e) {
// Otros errores (red, API, esquema no encontrado, etc.)
print('Error inesperado: $e');
}
Características Avanzadas
Prevención de Memory Leaks
El paquete gestiona automáticamente todas las subscriptions de Firebase:
FirebaseMessaging.onMessageFirebaseMessaging.onMessageOpenedAppFirebaseMessaging.onTokenRefresh
Todas se cancelan apropiadamente cuando se llama a dispose().
Protección contra Inicialización Múltiple
El método initializeApp() puede llamarse múltiples veces de forma segura. Las llamadas adicionales serán ignoradas con un log de debug.
Manejo de Background Messages
Los mensajes en background se procesan automáticamente incluso cuando la app está cerrada, gracias al handler @pragma('vm:entry-point') que se ejecuta en un isolate separado.
Verificación de Tokens Duplicados
Antes de crear un nuevo registro en MongoDB, el paquete verifica si el token ya existe para evitar duplicados. Esto optimiza el uso de recursos y mantiene la base de datos limpia.
Additional information
Notas técnicas
- El manejador de mensajes en background se ejecuta en un isolate separado
- Los mensajes se filtran por el campo
IdPushNotificationen data - El logging solo aparece en modo debug (
kDebugMode) - Todas las subscriptions se manejan internamente para prevenir memory leaks
- Los tokens se actualizan automáticamente en el servidor con información del dispositivo
- La consulta MongoDB utiliza escapado de caracteres para prevenir inyecciones
- Los métodos
_getDeviceInfo()y_getAppVersion()se ejecutan en paralelo para mejor performance - La colección
tokenFirebaseestá configurada enEnvironmentPushNotification.coleccionTokenFirebase - El orden de campos en
EnvironmentPushNotification.camposColeccionTokenFirebasees crítico y no debe alterarse - Los tokens se gestionan con estados
activoybloqueadopara mantener historial - El método
anularToken()elimina el token de FCM y marca registros como bloqueados en MongoDB - Las consultas de verificación filtran solo tokens con estado
activo - Los tokens bloqueados se mantienen en la base de datos para auditoría y análisis
Solución de Problemas Comunes
Error: "Firebase no está inicializado"
// Asegúrate de inicializar Firebase antes
await Firebase.initializeApp();
Error: "No se puede inicializar: la instancia ya fue disposed"
// Necesitas reiniciar la app. El singleton no puede ser reinicializado después de dispose()
Error: "No se pudo obtener esquema de la colección"
// Verifica que tu servidor tenga configurada la colección 'tokenFirebase'
// y que apiRest.obtieneEsquema() pueda acceder a ella
No recibo notificaciones en background
// Verifica que el mensaje incluya IdPushNotification en data
{
"notification": {
"title": "Título",
"body": "Mensaje"
},
"data": {
"IdPushNotification": "unique-id-123" // ← Requerido
}
}
Permisos de notificación denegados
// En iOS, los permisos solo se pueden solicitar una vez
// El usuario debe ir a Settings -> Tu App -> Notifications para habilitarlos
// En Android, se pueden solicitar múltiples veces
Token no se actualiza en el servidor
// Verifica:
// 1. Que la colección 'tokenFirebase' exista en MongoDB
// 2. Que el esquema sea correcto
// 3. Que apiRest tenga las credenciales correctas
// 4. Revisa los logs de debug para ver el error específico
Error: "Token ya existe en la base de datos"
// Esto es normal. El paquete verifica si el token existe antes de crear uno nuevo
// Este mensaje solo aparece en logs de debug y no es un error
Error al anular token
// Si anularToken() falla:
// 1. Verifica que haya un token actual almacenado (currentToken no vacío)
// 2. Verifica la conexión a MongoDB
// 3. Revisa los logs de debug para ver el error específico
// 4. Considera continuar con el logout incluso si falla (seguridad)
try {
await notificationProvider.anularToken(
apiRest: apiRest,
utilsDataApiRest: utilsDataApiRest,
);
} catch (e) {
print('Error al anular token: $e');
// Continuar con logout de todas formas
_procederConLogout();
}
Token bloqueado sigue recibiendo notificaciones
// Los tokens bloqueados en MongoDB NO bloquean el envío desde Firebase
// Para detener las notificaciones completamente debes:
// 1. Usar anularToken() que elimina el token del dispositivo
// 2. NO enviar notificaciones a tokens marcados como 'bloqueado' desde tu servidor
// 3. Filtrar tokens activos al enviar notificaciones masivas
Ejemplo de Implementación Completa con BLoC
Este ejemplo muestra cómo integrar el paquete en una aplicación Flutter usando el patrón BLoC para gestionar el estado de las notificaciones push.
1. Listener de Autenticación
Primero, configuramos los listeners en relación al login para inicializar Firebase o anular el token cuando el usuario cierra sesión:
///[AUTH]
BlocListener<AuthBloc, AuthState>(
listenWhen: (previous, current) => !current.isWorking,
listener: (context, state) {
if (state.error.isEmpty) {
if (state.accion == AuthBloc.blocOnLoginAuth) {
final estadoUsuario =
FlutterUtilsProvider.determinaValorCampo([
EnvironmentApiRest.estado,
], state.usuarioAuth);
if (estadoUsuario == EstadoRegistro.bloqueado.valor) {
///TODO - Mostrar dialogo de cuenta bloqueada
return;
}
final DataShpProvider shp = DataShpProvider();
if (state.isLoggedIn &&
!shp.keyLogin.startsWith(
EnvironmentApiRest.userFlutter,
)) {
//
context.read<ConfigAppBloc>().add(
OnInitPushNotificaciones(
usuarioAuth: state.usuarioAuth,
),
);
}
}
if (state.accion == AuthBloc.blocOnLogoutAuth) {
context.read<ConfigAppBloc>().add(OnAnulaTokenFirebase());
}
}
},
)
2. Eventos del BLoC
Define los eventos que se dispararán para manejar las notificaciones push:
///Evento que inicializa las push notificaciones de Firebase
class OnInitPushNotificaciones extends ConfigAppEvent {
final Map<String, dynamic> usuarioAuth;
OnInitPushNotificaciones({required this.usuarioAuth});
@override
List<Object> get props => [usuarioAuth];
}
///Evento que se lanza cuando llega una nueva notificacion desde Firebase
class OnNuevaNotificacionFirebase extends ConfigAppEvent {
final Map<String, dynamic> firebaseNotification;
OnNuevaNotificacionFirebase({required this.firebaseNotification});
@override
List<Object> get props => [firebaseNotification];
}
///Evento que se lanza cuando se cierra sesion entonces se debe anular el token
///de firebase para que no lleguen mas notificaciones a este usuario
class OnAnulaTokenFirebase extends ConfigAppEvent {
OnAnulaTokenFirebase();
@override
List<Object> get props => [];
}
3. Manejadores de Eventos en el BLoC
Implementa los métodos que responden a estos eventos:
Future<void> _onInitPushNotificaciones(
OnInitPushNotificaciones event,
Emitter<ConfigAppState> emit,
) async {
await _handleAction(onInitPushNotificaciones, emit, () async {
if (notificationFireBaseProvider.isInitialized) {
debugPrint(
'Push notifications ya inicializadas, se omite reinicialización.',
);
return;
}
debugPrint('🔔 notificationFireBaseProvider.initializeApp Inicializa');
await notificationFireBaseProvider.initializeApp(
apiRest: apiRest,
utilsDataApiRest: UtilsDataApiRest(apiRest: apiRest),
idUsuario: event.usuarioAuth[EnvironmentApiRest.idServer],
coleccionAuth: Environment.origenColeccion,
);
debugPrint('🔔 notificationFireBaseProvider.initializeApp');
notificationFireBaseProvider.messagesStream.listen((mensaje) {
debugPrint('🔔 Mensaje Firebase recibido: $mensaje');
add(OnNuevaNotificacionFirebase(firebaseNotification: mensaje));
});
});
}
Future<void> _onNuevaNotificacionFirebase(
OnNuevaNotificacionFirebase event,
Emitter<ConfigAppState> emit,
) async {
await _handleAction(onNuevaNotificacionFirebase, emit, () async {
emit(state.copyWith(firebaseNotification: event.firebaseNotification));
});
}
Future<void> _onAnulaTokenFirebase(
OnAnulaTokenFirebase event,
Emitter<ConfigAppState> emit,
) async {
await _handleAction(onAnulaTokenFirebase, emit, () async {
if (!notificationFireBaseProvider.isInitialized) {
debugPrint(
'Push notifications no están inicializadas, no hay nada que anular.',
);
return;
}
await notificationFireBaseProvider.anularToken(
apiRest: apiRest,
utilsDataApiRest: UtilsDataApiRest(apiRest: apiRest),
);
});
}
Flujo Completo
- Login: Cuando el usuario inicia sesión exitosamente, se dispara
OnInitPushNotificaciones - Inicialización: El BLoC inicializa el provider de notificaciones con el ID del usuario
- Escucha de Mensajes: Se suscribe al stream de mensajes y dispara
OnNuevaNotificacionFirebasecuando llega una notificación - Actualización de Estado: El evento actualiza el estado del BLoC con la nueva notificación
- Logout: Cuando el usuario cierra sesión, se dispara
OnAnulaTokenFirebaseque elimina el token y lo marca como bloqueado en MongoDB
Ventajas de este Patrón
- ✅ Separación de Responsabilidades: La lógica de notificaciones está separada de la UI
- ✅ Testeable: Los eventos y estados pueden ser fácilmente testeados
- ✅ Mantenible: Cambios en la lógica de notificaciones no afectan otros componentes
- ✅ Escalable: Fácil agregar nuevos eventos o estados relacionados con notificaciones
- ✅ Reactivo: La UI se actualiza automáticamente cuando cambia el estado
Arquitectura Interna
Flujo de Actualización de Token
┌─────────────────┐
│ Inicialización │
└────────┬────────┘
│
▼
┌─────────────────┐
│ Obtener Token │
└────────┬────────┘
│
▼
┌──────────────────────┐
│ Verificar Existencia │ ──► Si existe: Log y Retornar
└──────────┬───────────┘
│
▼ No existe
┌──────────────────────┐
│ Obtener Info Device │ ──┐
│ & App Version │ │ (Paralelo)
└──────────┬───────────┘ │
│ │
▼ ▼
┌──────────────────────────┐
│ Crear Documento MongoDB │
└──────────┬───────────────┘
│
▼
┌──────────────────────┐
│ Guardar en Servidor │
└──────────────────────┘
Licencia
Este proyecto está bajo la licencia especificada en el archivo LICENSE.
Versión
v3.0.0 - Versión estable para web con:
- ✅ Gestión automática de memoria
- ✅ Prevención de memory leaks
- ✅ Protección contra inicialización múltiple
- ✅ Manejo robusto de errores
- ✅ Actualización automática de tokens
- ✅ Integración con información del dispositivo
- ✅ Verificación de tokens existentes
- ✅ Configuración centralizada en
EnvironmentPushNotification - ✅ Escapado de consultas MongoDB para seguridad
- ✅ Performance optimizado con llamadas paralelas
- ✅ Método
getToken()para obtener token manualmente - ✅ Método
anularToken()para eliminar y bloquear tokens - ✅ Gestión de estados (activo/bloqueado) para tokens
- ✅ Historial de tokens con estado bloqueado
Changelog
Ver CHANGELOG.md para el historial completo de cambios.