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

Flutter Firebase通知点击无法跳转指定页面问题排查

Flutter Firebase通知点击无法跳转指定页面问题

我开发了一款Flutter应用,集成Firebase与flutter_local_notifications插件实现通知功能。目前应用能正常接收通知,点击通知可打开应用,但无法跳转到预期的指定页面。已简化代码排查问题,可能存在遗漏点,相关代码如下:

firebase_notification.helper.dart

import 'package:firebase_messaging/firebase_messaging.dart';
import 'package:flutter_local_notifications/flutter_local_notifications.dart';
import 'package:timezone/data/latest_all.dart' as timezone;
import 'package:timezone/timezone.dart' as timezone;

class FirebaseNotificationHelper {
  static final _notification = FlutterLocalNotificationsPlugin();

  static void init() {
    _notification.initialize(
      const InitializationSettings(
        android: AndroidInitializationSettings('@mipmap/ic_launcher'),
        iOS: DarwinInitializationSettings(),
      ),
    );
  }

  static pushNotification(
    RemoteMessage message,
  ) async {
    var androidPlatformChannelSpecifics = const AndroidNotificationDetails(
      'channel id',
      'channel name',
      channelDescription: 'channel description',
      importance: Importance.max,
      priority: Priority.high,
    );
    var iOSPlatformChannelSpecifics = const DarwinNotificationDetails();
    var platformChannelSpecifics = NotificationDetails(
      android: androidPlatformChannelSpecifics,
      iOS: iOSPlatformChannelSpecifics,
    );
    await _notification.show(1, message.notification?.title,
        message.notification?.body, platformChannelSpecifics);
  }
}

firebase.helper.dart

import 'dart:convert';

import 'package:firebase_core/firebase_core.dart';
import 'package:firebase_messaging/firebase_messaging.dart';
import 'package:flutter/material.dart';
import 'package:flutter_secure_storage/flutter_secure_storage.dart';
import 'package:grocstock/firebase_options.dart';
import 'package:grocstock/helpers/firebase_notification.helper.dart';
import 'package:grocstock/helpers/firestore.helper.dart';
import 'package:grocstock/helpers/page.helper.dart';
import 'package:grocstock/helpers/popup.helper.dart';
import 'package:grocstock/helpers/storage.helper.dart';
import 'package:grocstock/models/logged.user.model.dart';
import 'package:grocstock/modules/home/home.dart';
import 'package:grocstock/modules/preview_home/preview_home.dart';
import 'package:grocstock/services/house.service.dart';

initializeFirebase(BuildContext context) async {
  await Firebase.initializeApp(
    options: DefaultFirebaseOptions.currentPlatform,
  );

  FirebaseNotificationHelper.init();
  listenToMessages(context);
}

initFirebaseMessaging() async {
  FirebaseMessaging messaging = FirebaseMessaging.instance;

  try {
    String? token = await FirebaseMessaging.instance.getToken();
    const storage = FlutterSecureStorage();
    storage.write(key: 'fcmToken', value: token);
    await initFireBaseHouseUser(token);
  } catch (e) {
    print('Failed to get FCM token: $e');
  }

  await messaging.requestPermission();
}

listenToMessages(BuildContext context) {
  FirebaseMessaging.onMessage.listen((RemoteMessage message) async {
    if (message.notification != null) {
      print('Message also contained a notification: ${message.data}');
    }
    bool notificationsEnabled = await checkAppPopUpPermission();
    bool actionNotificationsEnabled = await checkActionAppPopUpPermission();
    bool messageNotificationsEnabled = await checkMessageAppPopUpPermission();

    if (!notificationsEnabled) return;

    String type = message.data['type'];
    bool showNotification = true;

    if (type == 'ACTION' && !actionNotificationsEnabled) {
      showNotification = false;
    } else if (type == 'MESSAGE' && !messageNotificationsEnabled) {
      showNotification = false;
    }

    if (showNotification) {
      await FirebaseNotificationHelper.pushNotification(message);
    }
  });

  FirebaseMessaging.onMessageOpenedApp.listen((RemoteMessage message) async {
    await _redirectToHouse(context, message);
  });

  FirebaseMessaging.instance.getInitialMessage().then((RemoteMessage? message) {
    if (message != null) {
      print(
          'App opened from terminated state by notification with data: ${message.data}');
      _redirectToHouse(context, message);
    }
  });
}

Future<void> _redirectToHouse(BuildContext context, RemoteMessage message) async {
  try {
    String? houseId = message.data['houseId'];
    if(houseId != null && houseId.isNotEmpty) {
      var house = await getHouse(context, houseId);
      var user = await safeReadFromStorage('user', null);
      var loggedUser = LoggedUser.fromJson(jsonDecode(user!));

      String isAdmin = message.data['isAdmin'];
      if(isAdmin == 'true') {
        print('Navigate to Saved house');
        redirectToPage(context, PreviewHomePage(house: house, user: loggedUser));
      } else {
        redirectToPage(context, HomePage(house: house, user: loggedUser));
        print('Navigate to actual house');
      }
    }
  } catch (e) {
    print(e);
  }
}

splash.page.dart

class SplashScreenState extends State<SplashScreen> {
  static final GlobalKey<NavigatorState> navigatorKey =
  GlobalKey(debugLabel: 'Main Navigator');
  DarkThemeProvider themeChangeProvider = DarkThemeProvider();

  @override
  void initState() {
    super.initState();
    _initializeFirebase();
  }
  _initializeFirebase() async {
    await initializeFirebase(context);
  }


  Future<bool> _checkTermsAccepted() async {
    SharedPreferences prefs = await SharedPreferences.getInstance();
    return prefs.getBool('termsAccepted') ?? false;
  }

  @override
  Widget build(BuildContext context) {
    return ChangeNotifierProvider(
      create: (_) {
        return themeChangeProvider;
      },
      child: Consumer<DarkThemeProvider>(
        builder: (BuildContext builderContext, DarkThemeProvider value,
            Widget? child) {
          return FutureBuilder<bool>(
            future: _checkTermsAccepted(),
            builder: (context, snapshot) {
              if (snapshot.connectionState == ConnectionState.waiting) {
                return const Center(child: CircularProgressIndicator());
              } else {
                bool termsAccepted = snapshot.data ?? false;
                return MaterialApp(
                  navigatorKey: navigatorKey,
                  title: 'Welcome',
                  debugShowCheckedModeBanner: false,
                  theme: Styles.themeData(
                      themeChangeProvider.darkTheme, builderContext),
                  home: termsAccepted
                      ? Login(shouldRedirect: true)
                      : const TermsAndConditionsPage(),
                  onGenerateRoute: generateRoute,
                  builder: BotToastInit(),
                  navigatorObservers: [BotToastNavigatorObserver()],
                );
              }
            },
          );
        },
      ),
    );
  }
}

问题场景

当前APP在以下三种场景下均无法正常跳转:

  • 应用完全关闭时
  • 应用处于后台(最小化)时
  • 应用处于前台时

依赖版本

flutter_local_notifications: ^17.2.1+2
firebase_core: ^3.4.0
cloud_firestore: ^5.3.0
firebase_messaging: ^15.1.0

问题分析与修复方案

1. FlutterLocalNotifications未处理前台通知点击回调

应用前台收到通知时,点击flutter_local_notifications展示的通知不会触发Firebase的消息回调,必须手动配置插件的点击回调,并传递消息数据。

修复:
修改FirebaseNotificationHelper类,添加点击回调并传入消息数据:

import 'dart:convert'; // 新增导入

class FirebaseNotificationHelper {
  static final _notification = FlutterLocalNotificationsPlugin();

  static void init() {
    _notification.initialize(
      const InitializationSettings(
        android: AndroidInitializationSettings('@mipmap/ic_launcher'),
        iOS: DarwinInitializationSettings(),
      ),
      // 新增通知点击回调
      onDidReceiveNotificationResponse: (NotificationResponse response) async {
        if (response.payload != null) {
          final Map<String, dynamic> data = jsonDecode(response.payload!);
          await _handleNotificationTap(data);
        }
      },
    );
  }

  static pushNotification(RemoteMessage message) async {
    var androidPlatformChannelSpecifics = const AndroidNotificationDetails(
      'channel id',
      'channel name',
      channelDescription: 'channel description',
      importance: Importance.max,
      priority: Priority.high,
    );
    var iOSPlatformChannelSpecifics = const DarwinNotificationDetails();
    var platformChannelSpecifics = NotificationDetails(
      android: androidPlatformChannelSpecifics,
      iOS: iOSPlatformChannelSpecifics,
    );
    // 将消息data转为json字符串作为payload传递
    String payload = jsonEncode(message.data);
    await _notification.show(
      1,
      message.notification?.title,
      message.notification?.body,
      platformChannelSpecifics,
      payload: payload,
    );
  }

  // 统一处理通知点击跳转
  static Future<void> _handleNotificationTap(Map<String, dynamic> data) async {
    try {
      String? houseId = data['houseId'];
      if (houseId != null && houseId.isNotEmpty) {
        var house = await getHouse(SplashScreenState.navigatorKey.currentContext!, houseId);
        var user = await safeReadFromStorage('user', null);
        var loggedUser = LoggedUser.fromJson(jsonDecode(user!));

        String isAdmin = data['isAdmin'];
        Widget page = isAdmin == 'true' 
            ? PreviewHomePage(house: house, user: loggedUser)
            : HomePage(house: house, user: loggedUser);

        // 使用全局navigatorKey跳转
        SplashScreenState.navigatorKey.currentState?.push(
          MaterialPageRoute(builder: (_) => page),
        );
      }
    } catch (e) {
      print(e);
    }
  }
}

2. 全局NavigatorKey使用不规范

应用从终止状态启动时,传入的context可能未完成初始化,必须使用全局navigatorKey处理所有跳转场景。

修复:
修改firebase.helper.dart中的监听方法,移除context依赖,统一使用全局navigatorKey:

// 修改initializeFirebase,移除context参数
initializeFirebase() async {
  await Firebase.initializeApp(
    options: DefaultFirebaseOptions.currentPlatform,
  );

  FirebaseNotificationHelper.init();
  listenToMessages();
}

// 修改listenToMessages,移除context参数
listenToMessages() {
  FirebaseMessaging.onMessage.listen((RemoteMessage message) async {
    // 原有逻辑不变
  });

  FirebaseMessaging.onMessageOpenedApp.listen((RemoteMessage message) async {
    await _redirectToHouse(message);
  });

  FirebaseMessaging.instance.getInitialMessage().then((RemoteMessage? message) {
    if (message != null) {
      print('App opened from terminated state by notification with data: ${message.data}');
      _redirectToHouse(message);
    }
  });
}

// 修改_redirectToHouse,使用全局navigatorKey
Future<void> _redirectToHouse(RemoteMessage message) async {
  try {
    String? houseId = message.data['houseId'];
    if(houseId != null && houseId.isNotEmpty) {
      var house = await getHouse(SplashScreenState.navigatorKey.currentContext!, houseId);
      var user = await safeReadFromStorage('user', null);
      var loggedUser = LoggedUser.fromJson(jsonDecode(user!));

      String isAdmin = message.data['isAdmin'];
      Widget page = isAdmin == 'true'
          ? PreviewHomePage(house: house, user: loggedUser)
          : HomePage(house: house, user: loggedUser);

      // 确保navigator已初始化,未就绪则延迟执行
      if (SplashScreenState.navigatorKey.currentState != null) {
        SplashScreenState.navigatorKey.currentState?.push(
          MaterialPageRoute(builder: (_) => page),
        );
      } else {
        Future.delayed(const Duration(milliseconds: 500), () {
          SplashScreenState.navigatorKey.currentState?.push(
            MaterialPageRoute(builder: (_) => page),
          );
        });
      }
    }
  } catch (e) {
    print(e);
  }
}

同时修改splash.page.dart中的初始化方法:

_initializeFirebase() async {
  await initializeFirebase(); // 不再传context
}

3. 路由跳转逻辑校验

检查redirectToPage方法,确保其使用全局navigatorKey而非传入的context,如果是自定义跳转方法,建议替换为直接使用navigatorKey的跳转逻辑。

额外注意事项

  • 打印message.data确认通知中确实包含houseId和isAdmin参数;
  • iOS平台需在Info.plist中配置远程通知权限,确保FirebaseMessaging权限请求成功;
  • 测试终止状态跳转时,需完全关闭应用(后台划掉)后再点击通知测试。

内容的提问来源于stack exchange,提问作者Rohith Poyyeri

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.06.17 03:04:51