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

Flutter报错:查找已停用组件的祖先不安全问题求助

问题描述

开发Flutter在线游戏房间,用MongoDB存储玩家和房间信息。两台设备可连接同一房间,但创建房间的设备(房主)离开时,出现以下报错:

E/flutter ( 4526): [ERROR:flutter/runtime/dart_vm_initializer.cc(41)] Unhandled Exception: Looking up a deactivated widget's ancestor is unsafe.
E/flutter ( 4526): At this point the state of the widget's element tree is no longer stable.
E/flutter ( 4526): 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.

怀疑问题出在socket_method.dart中的Navigator.pop(context),相关代码如下:

socket_method.dart

void leaveRoom(String roomId) {
  _socketClient.emit('leaveRoom', {'roomId': roomId});
}
void leaveRoomSuccessListener(BuildContext context) {
  _socketClient.on('leaveRoomSuccess', (data) {
    if (data['room'] != null) {
      Provider.of<RoomDataProvider>(context, listen: false)
          .updateRoomData(data['room']);
      Navigator.pop(context);
      showSnackBar(context, data['msg']);
    }
  });
}

index.js

socket.on('leaveRoom', async({roomId})=>{
  try{
      console.log(`leaving room ${roomId}`);
      let room = await Room.findOne({roomId: roomId});
      if(room){
          console.log(room)
          console.log(socket.id)
      }
      else{
          console.log('room not found')
      }
      

      const index = room.players.map((player) => player.socketId).indexOf(socket.id)
      console.log(index)
      const deletedPlayer = room.players[index];
      if (index > -1) { // only splice array when item is found
          room.players.splice(index, 1); // 2nd parameter means remove one item only
      }
      room.occupancy = room.players.length;
      // if room is empty, delete room
      if(room.occupancy === 0 || deletedPlayer.playerType === 'host'){
          io.to(roomId).emit('leaveRoomSuccess', {room, msg: 'Room has been closed'});
          await Room.deleteOne({roomId: roomId});
      }
      // else, update room
      else{
          room = await room.save();
          socket.leave(roomId);
          io.to(socket.id).emit('leaveRoomSuccess', {room, msg: 'You have left the room'});
      }
      
  }
  catch(err){
      console.log(err);
  }
})

create_room_screen.dart

class CreateRoomScreen extends StatefulWidget {
  static String routeName = '/create-room';
  const CreateRoomScreen({super.key});

  @override
  State<CreateRoomScreen> createState() => _CreateRoomScreenState();
}

class _CreateRoomScreenState extends State<CreateRoomScreen> {
  final TextEditingController _nameController = TextEditingController();
  final SocketMethods _socketMethods = SocketMethods();

  @override
  void initState() {
    super.initState();
    _socketMethods.createRoomSuccessListener(context);
    _socketMethods.checkStatus();
  }

  //prevent memory leaks
  @override
  void dispose() {
    super.dispose();
    _nameController.dispose();
  }

  @override
  Widget build(BuildContext context) {
    final size = MediaQuery.of(context).size;
    return Scaffold(
      body: Responsive(
        child: Container(
          margin: const EdgeInsets.symmetric(horizontal: 20),
          child: Column(
              mainAxisAlignment: MainAxisAlignment.center,
              crossAxisAlignment: CrossAxisAlignment.center,
              children: [
                const CustomText(text: 'Create Room', fontSize: 70, shadows: [
                  Shadow(
                    blurRadius: 20,
                    color: Colors.blue,
                  )
                ]),
                SizedBox(height: size.height * 0.1),
                CustomTextField(
                  controller: _nameController,
                  hintText: "Enter a name",
                ),
                SizedBox(height: size.height * 0.045),
                CustomButton(
                    onPressed: () => {
                          _socketMethods.createRoom(_nameController.text),
                        },
                    text: "Create")
              ]),
        ),
      ),
    );
  }
}

game_room_screen.dart

class GameScreen extends StatefulWidget {
  static String routeName = '/game';
  const GameScreen({super.key});

  @override
  State<GameScreen> createState() => _GameScreenState();
}

class _GameScreenState extends State<GameScreen> {
  final SocketMethods _socketMethods = SocketMethods();
  @override
  void initState() {
    super.initState();
    _socketMethods.leaveRoomSuccessListener(context);
  }

  @override
  void didChangeDependencies(){
    super.didChangeDependencies();
    
  }
  @override
  Widget build(BuildContext context) {
    final roomId = Provider.of<RoomDataProvider>(context).roomData['roomId'];
    return Scaffold(
      body: Center(
          child: Column(
        mainAxisAlignment: MainAxisAlignment.center,
        crossAxisAlignment: CrossAxisAlignment.center,
        children: [
          Text(Provider.of<RoomDataProvider>(context).roomData.toString()),
          CustomButton(
              onPressed: () => {_socketMethods.leaveRoom(roomId)},
              text: "Leave Room")
        ],
      )),
    );
  }
}
问题原因
  1. Context失效:房主离开时,服务器会给整个房间的所有客户端发送leaveRoomSuccess事件。当房主自己的GameScreen执行Navigator.pop(context)后,该Widget会被标记为已销毁,但Socket回调仍然持有原来的context,后续尝试用这个context操作导航或Provider就会触发报错。
  2. 未移除Socket监听:Widget销毁时没有取消对应的Socket事件监听,导致回调在Widget已销毁后仍被触发,访问无效的context。
修复方案

1. 重构Socket方法,让Widget管理监听与回调

修改socket_method.dart,将监听改为返回取消函数,由Widget在dispose时移除监听,同时通过回调传递事件,避免直接持有context:

void leaveRoom(String roomId) {
  _socketClient.emit('leaveRoom', {'roomId': roomId});
}

// 返回取消监听的函数,让Widget自行管理
Function leaveRoomSuccessListener(Function(Map data) onSuccess) {
  void listener(data) {
    onSuccess(data);
  }
  _socketClient.on('leaveRoomSuccess', listener);
  // 返回取消监听的函数
  return () => _socketClient.off('leaveRoomSuccess', listener);
}

2. 修改GameScreen,管理Socket监听并判断Widget状态

在GameScreen的State中保存取消监听的函数,在dispose时调用,同时操作context前先判断mounted状态:

class _GameScreenState extends State<GameScreen> {
  final SocketMethods _socketMethods = SocketMethods();
  late Function _cancelLeaveRoomListener;

  @override
  void initState() {
    super.initState();
    // 注册监听并保存取消函数
    _cancelLeaveRoomListener = _socketMethods.leaveRoomSuccessListener((data) {
      if (data['room'] != null && mounted) {
        Provider.of<RoomDataProvider>(context, listen: false)
            .updateRoomData(data['room']);
        Navigator.pop(context);
        showSnackBar(context, data['msg']);
      }
    });
  }

  @override
  void dispose() {
    // 取消Socket监听,避免内存泄漏和无效回调
    _cancelLeaveRoomListener();
    super.dispose();
  }

  @override
  Widget build(BuildContext context) {
    final roomId = Provider.of<RoomDataProvider>(context).roomData['roomId'];
    return Scaffold(
      body: Center(
          child: Column(
        mainAxisAlignment: MainAxisAlignment.center,
        crossAxisAlignment: CrossAxisAlignment.center,
        children: [
          Text(Provider.of<RoomDataProvider>(context).roomData.toString()),
          CustomButton(
              onPressed: () => _socketMethods.leaveRoom(roomId),
              text: "Leave Room")
        ],
      )),
    );
  }
}

3. 优化服务器逻辑(可选)

房主离开时,服务器给所有房间用户发事件,但房主已经执行离开操作,其实可以只给其他用户发事件,房主自己的逻辑在调用leaveRoom后直接处理:

// 修改index.js中房主离开的分支
if(room.occupancy === 0 || deletedPlayer.playerType === 'host'){
    // 只给非房主的其他房间用户发关闭事件
    socket.to(roomId).emit('leaveRoomSuccess', {room, msg: 'Room has been closed'});
    // 房主自己的逻辑可以在客户端调用leaveRoom后直接处理,不需要等待服务器回调
    await Room.deleteOne({roomId: roomId});
}

这样修改后,就能避免已销毁的Widget的context被访问,同时防止内存泄漏。

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.07.15 21:37:32