Flutter FCM通知在应用处于前台时,onTap回调不起作用;在后台时则可以工作
我正在使用Firebase Cloud Messaging与本地通知实现Flutter的推送通知。
问题在于:
- 当应用处于 后台 或 已终止 时,点击通知可以正常工作。
- 但当应用已经在前台运行时,我会收到通知,但点击它却没有任何反应。
- 点击回调中的日志/输出语句不会被打印。
在前台时,我是手动显示通知。
目前的行为
前台
- 通知会出现
- 点击回调不会触发
- 打印/日志未执行
后台
- 通知会出现
- 点击回调能够正确触发
Flutter包
firebase_messaging:
flutter_local_notifications:
来自后端的Firebase载荷
{
"message": {
"token": "DEVICE_TOKEN",
"notification": {
"title": "Test Title",
"body": "Test Body"
},
"data": {
"type": "chat",
"id": "123",
"taks_id: "123",
"taskable_id: "123"
}
}
}
import 'dart:convert';
import 'package:firebase_messaging/firebase_messaging.dart';
import 'package:flutter_local_notifications/flutter_local_notifications.dart';
import 'package:mre/utils/notification_router.dart';
final class LocalNotificationHelper {
static final FlutterLocalNotificationsPlugin _plugin =
FlutterLocalNotificationsPlugin();
static Future<void> init() async {
const android = AndroidInitializationSettings('@mipmap/retiq');
const ios = DarwinInitializationSettings();
const settings = InitializationSettings(android: android, iOS: ios);
await _plugin.initialize(
settings,
onDidReceiveNotificationResponse: (response) {
if (response.payload != null) {
final data = jsonDecode(response.payload!);
NotificationRouter.handle(data);
}
},
);
await _createChannels();
}
static Future<void> _createChannels() async {
const channel = AndroidNotificationChannel(
'high_importance_channel', 'High Importance Notifications',
importance: Importance.high,
enableLights: true,
sound: RawResourceAndroidNotificationSound("notification"));
await _plugin
.resolvePlatformSpecificImplementation<
AndroidFlutterLocalNotificationsPlugin>()
?.createNotificationChannel(channel);
}
static Future<void> showNotification(RemoteMessage message) async {
final data = message.data;
final title = data['title'] ?? message.notification?.title;
final body = data['body'] ?? message.notification?.body;
final androidDetails = AndroidNotificationDetails(
'high_importance_channel',
'High Importance Notifications',
importance: Importance.high,
priority: Priority.high,
);
const iosDetails = DarwinNotificationDetails();
await _plugin.show(
DateTime.now().microsecond,
title,
body,
NotificationDetails(
android: androidDetails,
iOS: iosDetails,
),
payload: jsonEncode(data),
);
}
}
import 'dart:convert';
import 'package:firebase_messaging/firebase_messaging.dart';
import 'package:mre/utils/notification_helper.dart';
import 'package:mre/utils/notification_router.dart';
class NotificationService {
static final FirebaseMessaging _messaging = FirebaseMessaging.instance;
static String? _lastHandledMessageId;
static Future<void> init() async {
await _requestPermission();
/// Foreground messages
FirebaseMessaging.onMessage.listen(_onForegroundMessage);
/// App opened from background
FirebaseMessaging.onMessageOpenedApp.listen(_onMessageOpened);
/// App opened from terminated state
final initialMessage = await _messaging.getInitialMessage();
if (initialMessage != null) {
_handleNavigation(initialMessage);
}
}
static Future<void> _requestPermission() async {
await _messaging.requestPermission(
alert: true,
badge: true,
sound: true,
);
}
/// Foreground → show local notification
static void _onForegroundMessage(RemoteMessage message) {
LocalNotificationHelper.showNotification(message);
}
/// Background tap
static void _onMessageOpened(RemoteMessage message) {
_handleNavigation(message);
}
/// Navigation handler (DEDUPLICATED)
static void _handleNavigation(RemoteMessage message) {
final messageId = message.messageId ?? jsonEncode(message.data);
if (_lastHandledMessageId == messageId) return;
_lastHandledMessageId = messageId;
NotificationRouter.handle(message.data);
}
}
import 'package:flutter/material.dart';
import 'package:mre/screens/opn_form/view/opn_form_view.dart';
import '../core/mk_route.dart';
import "../utils/context.dart";
class NotificationRouter {
static void handle(Map<String, dynamic> data) {
final context = GlobalContext.instance.navigatorKey?.currentContext;
if (context == null) return;
final type = data['type'] ?? "";
switch (type) {
case 'POS_ACCESS_REQUEST':
Navigator.pushNamed(context, RoutePage.accessRequest);
break;
case 'TASK':
Navigator.of(context).push(MaterialPageRoute(
builder: (context) {
return OpnFormScreen(
taskableId: data["taskable_id"],
taskId: data['task_id'],
);
},
));
break;
}
}
}
Future<void> main() async {
WidgetsFlutterBinding.ensureInitialized();
/// Disable rotation
if (Platform.isIOS || Platform.isAndroid) {
SystemChrome.setPreferredOrientations(
[DeviceOrientation.portraitUp, DeviceOrientation.portraitDown]);
}
runApp(AppRoot());
_initializeAsync();
}
void _initializeAsync() async {
await FirebaseIntegration().invoke();
await Future.wait([
CrashlyticsIntegration().invoke(),
UpGraderIntegration().invoke(),
LocalNotificationHelper.init(),
NotificationService.init()
]);
_requestLocationPermission();
}
解决方案
问题是在你的 main() 中存在一个 竞态条件。你在 LocalNotificationHelper.init() 尚未完成就调用了 runApp(),这意味着在点击发生时 onDidReceiveNotificationResponse 未注册。
dart
runApp(AppRoot()); // app starts immediately
_initializeAsync(); // init runs later, not awaited
将关键初始化放在 runApp() 之前:
dart
Future<void> main() async {
WidgetsFlutterBinding.ensureInitialized();
await FirebaseIntegration().invoke();
await LocalNotificationHelper.init(); // must finish before app renders
await NotificationService.init();
runApp(AppRoot());
// defer non-critical stuff
unawaited(CrashlyticsIntegration().invoke());
unawaited(UpGraderIntegration().invoke());
}
后台/终止的点击之所以能工作,是因为FCM会在操作系统层面处理这些,在你的应用代码实际运行之前就已经处理完毕。前台点击直接通过 flutter_local_notifications 处理,因此回调必须在任何点击发生之前注册。
此外,请确保已经为后台通知点击注册了 @pragma('vm:entry-point') 顶层函数,否则在发布构建时它会被tree-shaken(树摇优化)掉:
dart
@pragma('vm:entry-point')
void notificationTapBackground(NotificationResponse response) {
if (response.payload != null) {
final data = jsonDecode(response.payload!);
NotificationRouter.handle(data);
}
}
// inside LocalNotificationHelper.init():
await _plugin.initialize(
settings,
onDidReceiveNotificationResponse: (response) { ... },
onDidReceiveBackgroundNotificationResponse: notificationTapBackground,
);
站内所有文章版权归属LeftHeroAI导航站,无授权禁止任何主体转载、抄袭、复制内容,亦不得私自架设镜像站点。一经侵权,本站将通过法律途径追责。