You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

应用终止时无法接收Firebase Cloud Messaging推送通知求助

问题描述

已配置Firebase Cloud Messaging API(V1)服务端,实现点击通知图标按钮后5秒发送通知的功能:后台状态下可正常接收通知,但在5秒内关闭应用,应用终止状态下无法收到通知。需要解决应用终止状态下接收FCM推送的问题。


相关实现代码

服务端FCM工具类代码

import 'dart:convert';
import 'package:http/http.dart' as http;
import 'package:cloud_firestore/cloud_firestore.dart';
import 'package:googleapis_auth/auth_io.dart';

final FirebaseFirestore _firestore = FirebaseFirestore.instance;

final _credentials = ServiceAccountCredentials.fromJson({
  "type": "service_account",
  "project_id": "proyecto-chatgpt-84b99",
  "private_key_id": "",
  "private_key":
      "-----BEGIN PRIVATE KEY-----"
});

Future<String> getAccessToken() async {
  try {
    final client = http.Client();
    final AccessCredentials credentials =
        await obtainAccessCredentialsViaServiceAccount(_credentials,
            ['https://www.googleapis.com/auth/firebase.messaging'], client);

    final accessToken = credentials.accessToken.data;
    client.close();
    return accessToken;
  } catch (e) {
    print("Error obteniendo el token de acceso: $e");
    throw e;
  }
}

Future<void> sendPushNotification(
    String token, String title, String body) async {
  try {
    const String sendUrl =
        'https://fcm.googleapis.com/v1/projects/proyecto-chatgpt-84b99/messages:send';
    final String accessToken = await getAccessToken();

    final response = await http.post(
      Uri.parse(sendUrl),
      headers: {
        'Authorization': 'Bearer $accessToken',
        'Content-Type': 'application/json',
      },
      body: json.encode({
        'message': {
          'token': token,
          'notification': {
            'title': title,
            'body': body,
          },
          'android': {
            'priority': 'high',
            'notification': {
              'channelId': 'high_priority_channel',
            }
          },
        },
      }),
    );

    if (response.statusCode != 200) {
      throw Exception('Failed to send notification: ${response.body}');
    }
  } catch (e) {
    print("Error sending the notification: $e");
    throw e;
  }
}

Future<void> sendPushNotificationToUser(
    String userId, String title, String body) async {
  try {
    // Obtiene el token FCM del usuario desde Firestore
    final docSnapshot = await _firestore.collection('users').doc(userId).get();

    if (docSnapshot.exists && docSnapshot.data()?['fcmToken'] != null) {
      String userToken = docSnapshot.data()?['fcmToken'];
      await sendPushNotification(userToken, title, body);
    } else {
      print("Token FCM no encontrado para el usuario: $userId");
    }
  } catch (e) {
    print("Error enviando la notificación: $e");
    throw e;
  }
}

主程序代码

import 'dart:ui';

import 'package:chatgpt_project/src/authentication/auth_google.dart';
import 'package:chatgpt_project/src/fcm/fcm_helper.dart';
import 'package:chatgpt_project/src/notifications/schedule_notifications.dart';
import 'package:cloud_firestore/cloud_firestore.dart';
import 'package:flutter/material.dart';

import 'package:firebase_messaging/firebase_messaging.dart';
import 'package:flutter_local_notifications/flutter_local_notifications.dart';

import 'package:chatgpt_project/src/routes/routes.dart';
import 'package:firebase_core/firebase_core.dart';
import 'firebase_options.dart';
import 'package:intl/date_symbol_data_local.dart';
import 'package:provider/provider.dart';
import 'package:chatgpt_project/src/providers/chats_provider.dart';
import 'package:firebase_auth/firebase_auth.dart';

import 'package:timezone/data/latest.dart' as tz;
import 'package:timezone/timezone.dart' as tz;
import 'package:flutter/services.dart';
import 'package:shared_preferences/shared_preferences.dart';
import 'package:flutter_native_timezone/flutter_native_timezone.dart';

final FirebaseFirestore _firestore = FirebaseFirestore.instance;
final FlutterLocalNotificationsPlugin flutterLocalNotificationsPlugin = FlutterLocalNotificationsPlugin();

void main() async {
  WidgetsFlutterBinding.ensureInitialized();
  tz.initializeTimeZones();

  // Establecer la zona horaria local del dispositivo
  final String timeZoneName = await FlutterNativeTimezone.getLocalTimezone();
  tz.setLocalLocation(tz.getLocation(timeZoneName));

  await Firebase.initializeApp(
    options: DefaultFirebaseOptions.currentPlatform,
  );

  // 配置后台消息处理器
  FirebaseMessaging.onBackgroundMessage(firebaseMessagingBackgroundHandler);

  await initializeDateFormatting('es_ES', null);

  // 配置通知渠道
  const AndroidNotificationChannel channel = AndroidNotificationChannel(
    'high_priority_channel', // id
    'High Priority Notifications', // title
    description: 'Canal para notificaciones de alta prioridad',
    importance: Importance.high,
    playSound: true,
    showBadge: true,
  );

  await flutterLocalNotificationsPlugin
      .resolvePlatformSpecificImplementation<AndroidFlutterLocalNotificationsPlugin>()
      ?.createNotificationChannel(channel);

  // 设置默认通知渠道
  await FirebaseMessaging.instance.setForegroundNotificationPresentationOptions(
    alert: true,
    badge: true,
    sound: true,
  );

  // Inicializa Firebase Messaging
  _configureFirebaseListeners();

  final CallbackHandle callback =
      PluginUtilities.getCallbackHandle(callbackDispatcher)!;
  final int callbackHandle = callback.toRawHandle();
  final SharedPreferences prefs = await SharedPreferences.getInstance();
  await prefs.setInt('callback_handle', callbackHandle);

  runApp(
    MultiProvider(
      providers: [
        ChangeNotifierProvider(create: (context) => ChatProvider()),
      ],
      child: const MyApp(),
    ),
  );
}

// 后台消息处理器
Future<void> firebaseMessagingBackgroundHandler(RemoteMessage message) async {
  await Firebase.initializeApp(options: DefaultFirebaseOptions.currentPlatform);

  // 显示本地通知
  const AndroidNotificationDetails androidDetails = AndroidNotificationDetails(
    'high_priority_channel',
    'High Priority Notifications',
    channelDescription: 'Canal para notificaciones de alta prioridad',
    importance: Importance.high,
    priority: Priority.high,
    playSound: true,
  );
  const NotificationDetails notificationDetails = NotificationDetails(android: androidDetails);

  await flutterLocalNotificationsPlugin.show(
    message.hashCode,
    message.notification?.title,
    message.notification?.body,
    notificationDetails,
    payload: message.data.toString(),
  );
}

Future<void> updateTokenToFirestore(String token) async {
  // Obtiene el userId del usuario autenticado
  String? currentUserId = FirebaseAuth.instance.currentUser?.uid;

  if (currentUserId != null) {
    final docRef = _firestore.collection('users').doc(currentUserId);
    final docSnapshot = await docRef.get();

    if (docSnapshot.exists) {
      await docRef.update({'fcmToken': token});
    } else {
      await docRef.set({
        'fcmToken': token,
      });
    }
  }
}

void _configureFirebaseListeners() async {
  final FirebaseMessaging _firebaseMessaging = FirebaseMessaging.instance;

  // 请求通知权限
  NotificationSettings settings = await _firebaseMessaging.requestPermission(
    alert: true,
    announcement: false,
    badge: true,
    carPlay: false,
    criticalAlert: false,
    provisional: false,
    sound: true,
  );

  if (settings.authorizationStatus == AuthorizationStatus.authorized) {
    print('Usuario concedió permisos de notificación');
  }

  // Obtener y manejar el token de FCM
  _firebaseMessaging.getToken().then((String? token) {
    assert(token != null);
    print("Token de FCM: $token");
    updateTokenToFirestore(token!);
  });

  // Escuchar cambios en el token
  _firebaseMessaging.onTokenRefresh.listen((String token) {
    print("Token de FCM actualizado: $token");
    updateTokenToFirestore(token);
  });

  _firebaseMessaging.getInitialMessage().then((RemoteMessage? message) {
    if (message != null) {
      // Manejar la notificación al abrir la app desde estado terminado
      print('Notificación recibida al abrir la app: ${message.notification?.title}');
    }
  });

  FirebaseMessaging.onMessage.listen((RemoteMessage message) {
    // Manejar notificación en primer plano
    const AndroidNotificationDetails androidDetails = AndroidNotificationDetails(
      'high_priority_channel',
      'High Priority Notifications',
      channelDescription: 'Canal para notificaciones de alta prioridad',
      importance: Importance.high,
      priority: Priority.high,
      playSound: true,
    );
    const NotificationDetails notificationDetails = NotificationDetails(android: androidDetails);

    flutterLocalNotificationsPlugin.show(
      message.hashCode,
      message.notification?.title,
      message.notification?.body,
      notificationDetails,
      payload: message.data.toString(),
    );
  });

  FirebaseMessaging.onMessageOpenedApp.listen((RemoteMessage message) {
    // Manejar notificación al abrir la app desde segundo plano
    print('Notificación abierta desde segundo plano: ${message.notification?.title}');
  });
}

void fetchMedicinesFromBoot(List<String> args) async {
  String? userId = await getUserId();
  if (userId != null) {
    fetchMedicines(userId);
  }
}

void callbackDispatcher() {
  const MethodChannel _backgroundChannel =
      MethodChannel('com.example.chatgpt_project/background_channel');
  WidgetsFlutterBinding.ensureInitialized();

  _backgroundChannel.setMethodCallHandler((MethodCall call) async {
    if (call.method == 'fetchMedicinesFromBoot') {
      fetchMedicinesFromBoot([]);
    }
  });
}

class MyApp extends StatelessWidget {
  const MyApp({Key? key}) : super(key: key);

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'My App',
      debugShowCheckedModeBanner: false,
      initialRoute: Routes.loading,
      routes: Routes.routes,
    );
  }
}

测试按钮代码

child: IconButton(
          icon: const Icon(Icons.notifications,
              color: Colors.white, size: 33.0),
          onPressed: () async {
            try {
              // Obtiene el userId del usuario autenticado
              String? currentUserId =
                  FirebaseAuth.instance.currentUser?.uid;

              if (currentUserId != null) {
                print('Preparando para enviar notificación...');

                // Espera 5 segundos antes de enviar la notificación
                await Future.delayed(Duration(seconds: 5));

                // Envia una notificación de prueba usando el userId
                await sendPushNotificationToUser(
                    currentUserId,
                    'Notificación de prueba',
                    'Esta es una notificación de prueba');

                print('Notificación enviada exitosamente.');
              } else {
                print('No hay usuario autenticado.');
              }
            } catch (e) {
              print("Error al intentar enviar notificación de prueba: $e");
            }
          },
        ),

AndroidManifest.xml配置

<manifest xmlns:android="http://schemas.android.com/apk/res/android"
    package="com.example.chatgpt_project">

   <!-- Permiso para acceder a Internet -->
   <uses-permission android:name="android.permission.INTERNET" />

   <!-- Permisos para las notificaciones -->
   <uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED"/>
   <uses-permission android:name="android.permission.VIBRATE" />
   <uses-permission android:name="android.permission.POST_NOTIFICATIONS" />
   <uses-permission android:name="android.permission.WAKE_LOCK" />

   <application
        android:label="chatgpt_project"
        android:name="${applicationName}"
        android:icon="@mipmap/ic_launcher">
        <activity
            android:name=".MainActivity"
            android:exported="true"
            android:launchMode="singleTop"
            android:theme="@style/LaunchTheme"
            android:configChanges="orientation|keyboardHidden|keyboard|screenSize|smallestScreenSize|locale|layoutDirection|fontScale|screenLayout|density|uiMode"
            android:hardwareAccelerated="true"
            android:windowSoftInputMode="adjustResize">
            <meta-data
              android:name="io.flutter.embedding.android.NormalTheme"
              android:resource="@style/NormalTheme"
              />
            <intent-filter>
                <action android:name="android.intent.action.MAIN"/>
                <category android:name="android.intent.category.LAUNCHER"/>
            </intent-filter>
        </activity>
        <meta-data
            android:name="flutterEmbedding"
            android:value="2" />
        <!-- Receiver for handling boot completed to restore scheduled notifications after a reboot -->
        <receiver android:name="com.dexterous.flutterlocalnotifications.ScheduledNotificationBootReceiver" android:exported="false">
            <intent-filter>
                <action android:name="android.intent.action.BOOT_COMPLETED"></action>
                <action android:name="android.intent.action.MY_PACKAGE_REPLACED"></action>
            </intent-filter>
        </receiver>
        <receiver android:name="com.dexterous.flutterlocalnotifications.ScheduledNotificationReceiver" android:exported="false" />
        <!-- BootReceiver to handle re-scheduling notifications after device reboot -->
        <receiver android:name=".BootReceiver" android:enabled="true" android:exported="true">
            <intent-filter>
                <action android:name="android.intent.action.BOOT_COMPLETED"/>
                <category android:name="android.intent.category.DEFAULT" />
            </intent-filter>
        </receiver>

        <!-- Configuración para el canal de notificaciones de Firebase -->
        <meta-data
        android:name="com.google.firebase.messaging.default_notification_channel_id"
        android:value="high_priority_channel" />
    </application>
</manifest>

解决方案说明

要解决应用终止状态下无法接收FCM通知的问题,需要完成以下关键修改:

  1. 实现后台消息处理器

    • 启用FirebaseMessaging.onBackgroundMessage并实现对应的回调函数。应用终止状态下,FCM会触发这个后台回调,需在其中通过flutter_local_notifications插件显示本地通知。
  2. 配置flutter_local_notifications插件

    • 引入插件并创建高优先级通知渠道,确保通知能被系统正常显示;同时在前台消息监听逻辑中复用该插件,保持通知展示逻辑统一。
  3. 完善FCM消息Payload

    • 服务端发送消息时,为Android平台指定与本地渠道一致的channelId,确保通知关联到正确的高优先级渠道。
  4. 请求通知权限

    • 在FCM初始化流程中主动请求用户的通知权限,避免因权限缺失导致通知无法展示。
  5. 设备端检查

    • 确保设备未对应用启用电池优化,否则系统可能拦截后台消息推送。可引导用户在系统设置中关闭应用的电池优化选项。

内容的提问来源于stack exchange,提问作者newbie-flutter

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.07.09 04:35:53