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

Flutter FCM通知跳转页面时触发已销毁Widget祖先查找异常求助

问题:FCM通知跳转时触发"Looking up a deactivated widget's ancestor is unsafe"异常

我实现了单例的PushNotificationsManager类监听Firebase FCM通知,根据逻辑打开指定页面。在HomePage的initState中传入BuildContext初始化该类。

当点击前台通知弹窗或从后台点击通知唤醒应用时,调用navigateToChannelDetailsPage方法获取HomeCubit并跳转页面,此时触发异常:Looking up a deactivated widget's ancestor is unsafe。

HomePage初始化代码

class HomePage extends StatefulWidget {
  const HomePage({Key? key}) : super(key: key);

  @override
  State<HomePage> createState() => _HomePageState();
}

class _HomePageState extends State<HomePage> {
  @override
  void initState() {
    super.initState();
    PushNotificationsManager().listen(context);
  }

main.dart代码

Future<void> backgroundHandler(RemoteMessage message) async {
  print('Handling a background message ${message.messageId}');
  print('Content of message: ' + message.toString());
}

Future<void> main() async {
  WidgetsFlutterBinding.ensureInitialized();
  configureInjections();
  await Firebase.initializeApp(
    options: DefaultFirebaseOptions.currentPlatform,
  );
  FirebaseMessaging.onBackgroundMessage(backgroundHandler);
  runApp(SayApp());
}

PushNotificationsManager核心代码

@injectable
class PushNotificationsManager {
  static final PushNotificationsManager _instance = PushNotificationsManager._internal();

  factory PushNotificationsManager() {
    return _instance;
  }

  PushNotificationsManager._internal();

  Future<void> listen(BuildContext context) async {
    if (Platform.isIOS) {
      NotificationSettings settings = await FirebaseMessaging.instance.requestPermission(
        alert: true,
        badge: true,
        provisional: false,
        sound: true,
      );
      if (settings.authorizationStatus == AuthorizationStatus.authorized) {
        print('User granted permission');
        await _handleNotifications(context);
      }
    } else {
      await _handleNotifications(context);
    }
  }

  Future<void> _handleNotifications(BuildContext context) async {
    // 应用冷启动时的通知
    FirebaseMessaging.instance.getInitialMessage().then((message) {
      if (message != null) {
        openNotification(message, context);
      }
    });

    // 前台通知
    FirebaseMessaging.onMessage.listen((message) {
      if (message.notification != null) {
        _displayInAppNotifications(message);
      }
    });

    // 后台唤醒应用时的通知
    FirebaseMessaging.onMessageOpenedApp.listen((message) {
      openNotification(message, context);
    });
  }

  void _displayInAppNotifications(RemoteMessage message) {
    FlutterRingtonePlayer.playNotification();
    showOverlayNotification((context) {
      return GestureDetector(
        onTap: () {
          openNotification(message, context);
          OverlaySupportEntry.of(context)!.dismiss();
        },
        child: SafeArea(
          child: Card(
            child: ListTile(
              leading: message.data[NotificationType.NOTIFICATION_TYPE] ==
                      NotificationType.PRIVATE_MESSAGE_1_ON_1
                  ? Avatar(text: message.notification!.title!)
                  : SizedBox.fromSize(
                      size: const Size(40, 40),
                      child: ClipOval(
                          child: Container(
                        child: SvgPicture.asset(SvgIcons.sayAppWaveLogo),
                      ))),
              title:
                  Text(message.notification!.title!, overflow: TextOverflow.ellipsis, maxLines: 1),
              subtitle: Text(
                message.notification!.body!,
                overflow: TextOverflow.ellipsis,
                maxLines: 2,
              ),
              trailing: IconButton(
                  icon: Icon(Icons.close),
                  onPressed: () {
                    OverlaySupportEntry.of(context)!.dismiss();
                  }),
            ),
          ),
        ),
      );
    }, duration: Duration(seconds: 4), position: NotificationPosition.top);
  }

  Future<void> navigateToChannelDetailsPage(ChannelEntity channel, BuildContext context) async {
    var cubit = BlocProvider.of<HomeCubit>(context);
    var isUserChannelMember = await cubit.isCurrentUserChannelMember(channel);

    if (!isUserChannelMember) return;

    var isUserMutedPublic = await cubit.getIsUserMutedPublic(channel);

    if (isUserMutedPublic) channel.isUserMutedPublic = isUserMutedPublic;

    return PageNavigator.navigate(
            context, ChannelDetailsPage(channel: channel, isFromAdminNotification: true))
        .then((_) {
      FocusManager.instance.primaryFocus?.unfocus();
      cubit.updateChannelMemberChatDetailsDataOnChannelClosed(channel);
    });
  }

异常信息

E/flutter (25412): [ERROR:flutter/runtime/dart_vm_initializer.cc(41)] Unhandled Exception: Looking up a deactivated widget's ancestor is unsafe.
E/flutter (25412): At this point the state of the widget's element tree is no longer stable.
E/flutter (25412): To safely refer to a widget's ancestor in its dispose() method, save a reference to the ancestor by calling dependOnInheritedWidgetOfExactType() in the widget's didChangeDependencies() method.
E/flutter (25412): #0      Element._debugCheckStateIsActiveForAncestorLookup.<anonymous closure> (package:flutter/src/widgets/framework.dart:4241:9)
E/flutter (25412): #1      Element._debugCheckStateIsActiveForAncestorLookup (package:flutter/src/widgets/framework.dart:4255:6)
E/flutter (25412): #2      Element.getElementForInheritedWidgetOfExactType (package:flutter/src/widgets/framework.dart:4286:12)
E/flutter (25412): #3      Provider._inheritedElementOf (package:provider/src/provider.dart:339:38)
E/flutter (25412): #4      Provider.of (package:provider/src/provider.dart:293:30)
E/flutter (25412): #5      BlocProvider.of (package:flutter_bloc/src/bloc_provider.dart:100:23)
E/flutter (25412): #6      PushNotificationsManager.navigateToChannelDetailsPage (package:say_app/presentation/notification/push_notifications_manager.dart:204:30)
E/flutter (25412): #7      PushNotificationsManager.openNotification (package:say_app/presentation/notification/push_notifications_manager.dart:180:9)
E/flutter (25412): <asynchronous suspension>
解决方案

异常核心原因:单例PushNotificationsManager持有了HomePage的BuildContext,当HomePage被销毁(如页面跳转后回收),该context已失效,此时用它获取BlocProvider就会触发错误。

具体修复步骤:

  1. 移除单例对Widget BuildContext的持有
    修改listen和_handleNotifications方法,去掉context参数,不再将页面上下文保存到单例中。

  2. 配置全局导航Key
    在main.dart中定义全局GlobalKey<NavigatorState>,用来获取当前有效的导航上下文:

    final GlobalKey<NavigatorState> navigatorKey = GlobalKey<NavigatorState>();
    
    Future<void> main() async {
      WidgetsFlutterBinding.ensureInitialized();
      configureInjections();
      await Firebase.initializeApp(
        options: DefaultFirebaseOptions.currentPlatform,
      );
      FirebaseMessaging.onBackgroundMessage(backgroundHandler);
      runApp(SayApp(navigatorKey: navigatorKey));
    }
    

    在SayApp中将该Key传入MaterialApp:

    class SayApp extends StatelessWidget {
      final GlobalKey<NavigatorState> navigatorKey;
      const SayApp({required this.navigatorKey, Key? key}) : super(key: key);
    
      @override
      Widget build(BuildContext context) {
        return MaterialApp(
          navigatorKey: navigatorKey,
          // 其他MaterialApp配置项
        );
      }
    }
    
  3. 修改PushNotificationsManager的上下文获取逻辑
    通过全局导航Key获取当前活跃上下文,同时在异步操作前后检查上下文状态:

    // 引入全局navigatorKey,根据实际路径调整import
    import '../main.dart';
    
    @injectable
    class PushNotificationsManager {
      static final PushNotificationsManager _instance = PushNotificationsManager._internal();
    
      factory PushNotificationsManager() {
        return _instance;
      }
    
      PushNotificationsManager._internal();
    
      Future<void> listen() async {
        if (Platform.isIOS) {
          NotificationSettings settings = await FirebaseMessaging.instance.requestPermission(
            alert: true,
            badge: true,
            provisional: false,
            sound: true,
          );
          if (settings.authorizationStatus == AuthorizationStatus.authorized) {
            print('User granted permission');
            await _handleNotifications();
          }
        } else {
          await _handleNotifications();
        }
      }
    
      Future<void> _handleNotifications() async {
        FirebaseMessaging.instance.getInitialMessage().then((message) {
          if (message != null) {
            openNotification(message);
          }
        });
    
        FirebaseMessaging.onMessage.listen((message) {
          if (message.notification != null) {
            _displayInAppNotifications(message);
          }
        });
    
        FirebaseMessaging.onMessageOpenedApp.listen((message) {
          openNotification(message);
        });
      }
    
      void _displayInAppNotifications(RemoteMessage message) {
        FlutterRingtonePlayer.playNotification();
        showOverlayNotification((overlayContext) {
          return GestureDetector(
            onTap: () {
              openNotification(message);
              OverlaySupportEntry.of(overlayContext)!.dismiss();
            },
            child: SafeArea(
              child: Card(
                child: ListTile(
                  leading: message.data[NotificationType.NOTIFICATION_TYPE] ==
                          NotificationType.PRIVATE_MESSAGE_1_ON_1
                      ? Avatar(text: message.notification!.title!)
                      : SizedBox.fromSize(
                          size: const Size(40, 40),
                          child: ClipOval(
                              child: Container(
                            child: SvgPicture.asset(SvgIcons.sayAppWaveLogo),
                          ))),
                  title:
                      Text(message.notification!.title!, overflow: TextOverflow.ellipsis, maxLines: 1),
                  subtitle: Text(
                    message.notification!.body!,
                    overflow: TextOverflow.ellipsis,
                    maxLines: 2,
                  ),
                  trailing: IconButton(
                      icon: Icon(Icons.close),
                      onPressed: () {
                        OverlaySupportEntry.of(overlayContext)!.dismiss();
                      }),
                ),
              ),
            ),
          );
        }, duration: Duration(seconds: 4), position: NotificationPosition.top);
      }
    
      Future<void> navigateToChannelDetailsPage(ChannelEntity channel) async {
        final context = navigatorKey.currentContext;
        if (context == null || !context.mounted) return;
    
        var cubit = BlocProvider.of<HomeCubit>(context);
        var isUserChannelMember = await cubit.isCurrentUserChannelMember(channel);
    
        if (!isUserChannelMember) return;
    
        var isUserMutedPublic = await cubit.getIsUserMutedPublic(channel);
    
        if (isUserMutedPublic) channel.isUserMutedPublic = isUserMutedPublic;
    
        if (!context.mounted) return;
        return PageNavigator.navigate(
                context, ChannelDetailsPage(channel: channel, isFromAdminNotification: true))
            .then((_) {
          FocusManager.instance.primaryFocus?.unfocus();
          if (context.mounted) {
            cubit.updateChannelMemberChatDetailsDataOnChannelClosed(channel);
          }
        });
      }
    }
    
  4. 更新HomePage的初始化代码
    调用listen时不再传入context:

    class _HomePageState extends State<HomePage> {
      @override
      void initState() {
        super.initState();
        PushNotificationsManager().listen();
      }
    }
    

额外注意事项:

  • 所有异步操作完成后,必须检查context.mounted,避免在已销毁的上下文上执行操作。
  • 禁止在单例或长期存活对象中持有Widget的BuildContext,Widget生命周期短暂,上下文随时可能失效。
  • 若使用依赖注入框架(如get_it),可将navigatorKey注册为单例,让PushNotificationsManager通过注入获取,无需直接引用main.dart。

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.08.03 18:20:30