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") ], )), ); } }
- Context失效:房主离开时,服务器会给整个房间的所有客户端发送
leaveRoomSuccess事件。当房主自己的GameScreen执行Navigator.pop(context)后,该Widget会被标记为已销毁,但Socket回调仍然持有原来的context,后续尝试用这个context操作导航或Provider就会触发报错。 - 未移除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

