Flutter Firestore聊天应用:单条消息触发两次监听且时间戳异常
问题背景
开发聊天应用,流程为在TextField输入消息后点击按钮保存至Firestore数据库。发送一条消息后,数据库仅存储一条消息,但监听新消息的listen方法触发两次,且两次返回的时间戳不同。
相关代码片段
1. 消息发送代码
var _mesajCont = TextEditingController(); ... onPressed: () async { if (_mesajCont.text.trim().length > 0) { Mesaj _kaydedilecMesaj = Mesaj( bendenMi: true, message: _mesajCont.text, kimden: _chatModel.currentUser!.userID, kime: _chatModel.sohbetedilenUser!.userID); var sonuc = await _chatModel.saveMessage(_kaydedilecMesaj); // 保存消息 if (sonuc) { _mesajCont.clear(); } } },
2. Firestore服务代码(保存+获取消息)
Future<bool> saveMessage(Mesaj kaydedilecMesaj) async { var mesajID = _firestoreDB.collection("konusmalar").doc().id; var _myDocID = kaydedilecMesaj.kimden + "--" + kaydedilecMesaj.kime; var _alici = kaydedilecMesaj.kime + "--" + kaydedilecMesaj.kimden; var _kaydedilecMap = kaydedilecMesaj .toMap(); // bundan nesne üretince false sorunu düzeldi ilginc await _firestoreDB .collection("konusmalar") .doc(_myDocID) .collection("mesajlar") .doc(mesajID) .set(_kaydedilecMap); await _firestoreDB.collection("konusmalar").doc(_myDocID).set({ "konusma_sahibi": kaydedilecMesaj.kimden, "kimle_konusuyor": kaydedilecMesaj.kime, "son_yollanan_mesaj": kaydedilecMesaj.message, "konusma_goruldu": false, "olusturulma_tarihi": FieldValue.serverTimestamp(), }); _kaydedilecMap.update("bendenMi", (value) => false); await _firestoreDB .collection("konusmalar") .doc(_alici) .collection("mesajlar") .doc(mesajID) .set(_kaydedilecMap); await _firestoreDB.collection("konusmalar").doc(_alici).set({ "konusma_sahibi": kaydedilecMesaj.kime, "kimle_konusuyor": kaydedilecMesaj.kimden, "son_yollanan_mesaj": kaydedilecMesaj.message, "konusma_goruldu": false, "olusturulma_tarihi": FieldValue.serverTimestamp(), }); return true; } Stream<List<Mesaj>> getMassage( String currentUserID, String sohbetEdilenUserID) { var snapShot = _firestoreDB .collection("konusmalar") .doc(currentUserID + "--" + sohbetEdilenUserID) .collection("mesajlar") //.where("konusma_sahibi", isEqualTo: currentUserID) .orderBy("date", descending: true /*tarihi son olanı öne koyar*/) .limit(1) .snapshots(); return snapShot.map((mesajListesi) => mesajListesi.docs.map((mesaj) => Mesaj.fromMap(mesaj.data())).toList()); }
3. UI监听与消息列表渲染
Widget _buildMesajListesi() { return Consumer<ChatViewModel>(builder: (context, value, child) { return Expanded( child: ListView.builder( reverse: true, controller: _scrollCont, itemBuilder: ((context, index) { return talkingBalloon(value.mesajlarListesi![index]); // 新消息气泡 }), itemCount: value.dahaVarmii ? value.mesajlarListesi!.length + 1 : value.mesajlarListesi!.length, ), ); }); }
4. ViewModel中的监听逻辑
List<Mesaj>? _tumMesajlar; List<Mesaj>? get mesajlarListesi => _tumMesajlar; ..... void yeniMesajListenerGet() { _userRepository .getMessage(currentUser!.userID, sohbetedilenUser!.userID) .listen((anlikData) { debugPrint("LISTENER GET DATA>>>>${anlikData[0].toString()}"); if (anlikData[0] != null) { if (_listeyeilkMesaj!.date!.millisecondsSinceEpoch != anlikData[0].date!.millisecondsSinceEpoch) { _tumMesajlar!.insert(0, anlikData[0]); } } }); }
问题现象
发送一条消息后,控制台输出两次监听日志,时间戳不同:
LISTENER GET DATA>>>>WHO XzAHsfvxmYUqHirg5eH7ifT0Qtf1, TO WHO WoxT4Nxomeh1MQOE6p8eOadgRYa2, ME-> true, MESSAGE same message but diff timeStamp, TİMESTAMP Timestamp(seconds=1679296897, nanoseconds=792506000)
LISTENER GET DATA>>>>WHO XzAHsfvxmYUqHirg5eH7ifT0Qtf1, TO WHO WoxT4Nxomeh1MQOE6p8eOadgRYa2, ME-> true, MESSAGE same message but diff timeStamp, TİMESTAMP Timestamp(seconds=1679296914,nanoseconds=30000000)
数据库中仅存在一条消息,但两次监听返回的时间戳不一致。
原因分析
Firestore的FieldValue.serverTimestamp()存在两次写入机制:
- 客户端写入时,先使用本地生成的临时时间戳占位,立即触发监听回调;
- 服务器确认写入后,将临时时间戳替换为真实的服务器时间,文档更新再次触发监听回调。
你的监听逻辑是获取mesajlar集合的最新一条消息,当时间戳从临时值更新为服务器真实值时,会再次触发监听,导致同一消息被处理两次。
解决方案
方案一:使用客户端时间戳替代服务器时间戳
在创建Mesaj对象时直接使用客户端当前时间作为date字段,避免Firestore的二次更新:
Mesaj _kaydedilecMesaj = Mesaj( bendenMi: true, message: _mesajCont.text, kimden: _chatModel.currentUser!.userID, kime: _chatModel.sohbetedilenUser!.userID, date: DateTime.now() // 直接使用客户端当前时间 );
此方案简单直接,消息文档只会写入一次,不会触发二次监听。
方案二:通过消息ID过滤重复监听
修改ViewModel中的监听逻辑,以mesajID作为唯一判断依据(需确保Mesaj类包含mesajID字段并正确序列化):
void yeniMesajListenerGet() { _userRepository .getMessage(currentUser!.userID, sohbetedilenUser!.userID) .listen((anlikData) { debugPrint("LISTENER GET DATA>>>>${anlikData[0].toString()}"); if (anlikData[0] != null) { // 检查列表中是否已存在相同mesajID的消息 bool isDuplicate = _tumMesajlar!.any((mesaj) => mesaj.mesajID == anlikData[0].mesajID); if (!isDuplicate) { _tumMesajlar!.insert(0, anlikData[0]); } } }); }
此方案保留服务器时间戳的同时,避免重复插入同一条消息。
方案三:优化监听逻辑,获取完整消息集合快照
去掉getMassage方法中的limit(1),监听整个消息集合的变更,通过文档快照的类型判断是新增还是更新:
Stream<List<Mesaj>> getMassage( String currentUserID, String sohbetEdilenUserID) { var snapShot = _firestoreDB .collection("konusmalar") .doc(currentUserID + "--" + sohbetEdilenUserID) .collection("mesajlar") .orderBy("date", descending: true) .snapshots(); // 移除limit(1) return snapShot.map((mesajListesi) => mesajListesi.docs.map((mesaj) => Mesaj.fromMap(mesaj.data())).toList()); }
之后在ViewModel中直接更新整个消息列表,避免单条消息更新导致的重复触发。
内容的提问来源于stack exchange,提问作者LOODAN

