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

Flutter多人游戏:如何可靠检测App终止并更新Firebase玩家状态?

解决方案:可靠检测App终止并同步玩家状态

优先方案:Firebase Realtime Database .onDisconnect() 机制

这是Firebase官方针对客户端断开连接场景的原生解决方案,不受Flutter生命周期回调的限制,无论App是正常退出、被用户强制关闭还是系统杀进程,只要客户端与Realtime Database的TCP连接断开,服务器就会自动执行你预设的断开操作,完全可靠。

步骤1:Flutter端配置在线状态与断开回调

当玩家进入活跃队伍时,在Realtime Database写入在线状态,同时配置断开连接时的触发操作:

import 'package:firebase_database/firebase_database.dart';

void setupPlayerOnlineStatus(String playerId) async {
  final playerOnlineRef = FirebaseDatabase.instance.ref('players/$playerId/online');
  
  // 设置当前玩家为在线状态
  await playerOnlineRef.set(true);
  
  // 配置断开连接时的操作:移除在线节点(或设置为false)
  await playerOnlineRef.onDisconnect().remove();
  // 也可以选择设置为false:await playerOnlineRef.onDisconnect().set(false);
}

步骤2:Cloud Function监听状态变化并更新Firestore

编写Cloud Function监听Realtime Database的在线节点变化,当节点被移除(或状态变为false)时,自动从Firestore的活跃队伍中移除该玩家:

const functions = require("firebase-functions");
const admin = require("firebase-admin");
admin.initializeApp();

// 监听在线节点被删除的场景
exports.removePlayerOnDisconnect = functions.database.ref('/players/{playerId}/online')
  .onDelete(async (snapshot, context) => {
    const playerId = context.params.playerId;
    
    // 查找该玩家所在的活跃队伍
    const teamQuery = await admin.firestore()
      .collection('teams')
      .where('activePlayers', 'array-contains', playerId)
      .get();
    
    // 更新所有包含该玩家的队伍
    if (!teamQuery.empty) {
      const batch = admin.firestore().batch();
      teamQuery.docs.forEach(teamDoc => {
        batch.update(teamDoc.ref, {
          activePlayers: admin.firestore.FieldValue.arrayRemove(playerId)
        });
      });
      await batch.commit();
    }
  });

如果选择用.onDisconnect().set(false),可以改为监听节点更新:

exports.removePlayerOnOffline = functions.database.ref('/players/{playerId}/online')
  .onUpdate(async (change, context) => {
    const isOnline = change.after.val();
    if (!isOnline) {
      const playerId = context.params.playerId;
      // 同上执行队伍更新逻辑
      const teamQuery = await admin.firestore()
        .collection('teams')
        .where('activePlayers', 'array-contains', playerId)
        .get();
      
      if (!teamQuery.empty) {
        const batch = admin.firestore().batch();
        teamQuery.docs.forEach(teamDoc => {
          batch.update(teamDoc.ref, {
            activePlayers: admin.firestore.FieldValue.arrayRemove(playerId)
          });
        });
        await batch.commit();
      }
    }
  });

备选方案:Firestore心跳+定时清理(适合无法使用Realtime Database的场景)

如果必须全程用Firestore,可以采用心跳机制配合定时Cloud Function清理,但存在最多1分钟的延迟,资源消耗也更高:

步骤1:Flutter端发送心跳

玩家在线时每隔30秒向Firestore写入心跳时间戳:

import 'package:cloud_firestore/cloud_firestore.dart';
import 'dart:async';

Timer? _heartbeatTimer;

void startPlayerHeartbeat(String playerId) {
  _heartbeatTimer = Timer.periodic(const Duration(seconds: 30), (timer) async {
    await FirebaseFirestore.instance.collection('players').doc(playerId).update({
      'lastHeartbeat': FieldValue.serverTimestamp(),
      'online': true
    });
  });
}

// App进入后台时可选取消定时器,不影响最终清理逻辑
void stopPlayerHeartbeat() {
  _heartbeatTimer?.cancel();
}

步骤2:Cloud Function定时清理离线玩家

创建每分钟执行一次的定时任务,扫描心跳超过1分钟的玩家并从队伍中移除:

exports.cleanupInactivePlayers = functions.pubsub.schedule('every 1 minutes').onRun(async (context) => {
  const oneMinuteAgo = admin.firestore.Timestamp.fromMillis(Date.now() - 60 * 1000);
  
  // 查询心跳超时的玩家
  const inactivePlayersQuery = await admin.firestore()
    .collection('players')
    .where('lastHeartbeat', '<', oneMinuteAgo)
    .get();
  
  const batch = admin.firestore().batch();
  
  // 批量更新队伍和玩家状态
  for (const playerDoc of inactivePlayersQuery.docs) {
    const playerId = playerDoc.id;
    
    // 查找玩家所在队伍并移除
    const teamQuery = await admin.firestore()
      .collection('teams')
      .where('activePlayers', 'array-contains', playerId)
      .get();
    
    teamQuery.docs.forEach(teamDoc => {
      batch.update(teamDoc.ref, {
        activePlayers: admin.firestore.FieldValue.arrayRemove(playerId)
      });
    });
    
    // 更新玩家状态为离线
    batch.update(playerDoc.ref, {online: false});
  }
  
  await batch.commit();
  return null;
});

为什么之前的AppLifecycleState.detached不可靠?

Flutter的生命周期回调仅在App正常退到后台或前台时稳定触发,当App被用户强制关闭、系统因资源不足杀进程时,detached状态可能根本来不及执行,导致Realtime Database的在线状态无法更新,进而Cloud Function无法触发。而.onDisconnect()是由Firebase服务器端检测连接断开,完全不受客户端生命周期的限制,是这类场景的最优解。

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.06.16 09:05:08