Flutter中Firebase Cloud Firestore的listen监听在数据变更时重复触发两次问题排查
问题分析与解决方案
首先先给你吃个定心丸:你的场景下使用.listen()是完全合理的!StreamBuilder主要是用于将数据流和UI组件绑定、自动更新UI,而你不需要更新UI,只是要监听数据变更执行业务逻辑,用.listen()完全没问题,不用换成StreamBuilder~
接下来说说为什么会出现打印两次的情况:Firestore的snapshots()流默认会触发两次回调——第一次返回本地缓存的数据,第二次返回服务器同步后的最新数据。哪怕你只修改了一次文档,这个双回调是Firestore的默认设计,用来保证离线时也能快速获取数据,之后再同步服务器的最终结果。
解决办法
你可以通过快照的metadata.isFromCache属性区分这两种情况,根据需求选择只处理其中一次:
方案1:只处理服务器返回的最终数据
如果你的业务逻辑只需要响应服务器的最终变更,跳过本地缓存的回调,可以在回调开头加判断:
static Future<void> checkForRequests() async { print('Request call initiated'); try { _firestore .collection('Requests') .where('UsersList', arrayContains: _auth.currentUser.uid) .snapshots() .listen((event) { // 跳过本地缓存的快照,只处理服务器返回的结果 if (event.metadata.isFromCache) { return; } event.docs.forEach((element) { print('Checking for requests'); final person = element.get('ActionRequiredBy'); if (person == _auth.currentUser.uid) { final stage = element.get('Stage'); final requestID = element.get('ConversationID'); switch (stage) { case 3: { JPAKEStages.stage3(requestID); } break; case 4: { JPAKEStages.stage4(requestID); } break; case 5: { JPAKEStages.stage5(requestID); } break; case -1:{ } break; default: { return; } } } return; }); }); } catch (exception) { print(exception); } }
方案2:处理缓存但避免重复执行
如果你需要离线时也能触发逻辑,但不想重复执行,可以记录已经处理过的文档的updateTime,每次回调时对比,只处理更新过的内容:
// 维护一个全局变量记录已处理文档的最新更新时间 final Map<String, Timestamp> _processedDocs = {}; static Future<void> checkForRequests() async { print('Request call initiated'); try { _firestore .collection('Requests') .where('UsersList', arrayContains: _auth.currentUser.uid) .snapshots() .listen((event) { event.docs.forEach((element) { final docId = element.id; final updateTime = element.metadata.updateTime; // 如果该文档已经处理过且时间未更新,直接跳过 if (_processedDocs.containsKey(docId) && _processedDocs[docId] == updateTime) { return; } // 更新记录的最新时间 _processedDocs[docId] = updateTime; print('Checking for requests'); // 后续业务逻辑不变 final person = element.get('ActionRequiredBy'); if (person == _auth.currentUser.uid) { final stage = element.get('Stage'); final requestID = element.get('ConversationID'); switch (stage) { case 3: { JPAKEStages.stage3(requestID); } break; case 4: { JPAKEStages.stage4(requestID); } break; case 5: { JPAKEStages.stage5(requestID); } break; case -1:{ } break; default: { return; } } } return; }); }); } catch (exception) { print(exception); } }
额外提醒
如果你的checkForRequests()方法可能被多次调用,记得用StreamSubscription管理监听,避免创建多个重复的监听流导致更多重复执行:
// 定义全局订阅对象 StreamSubscription? _requestSubscription; static Future<void> checkForRequests() async { print('Request call initiated'); // 先取消之前的订阅(如果存在) _requestSubscription?.cancel(); try { _requestSubscription = _firestore .collection('Requests') .where('UsersList', arrayContains: _auth.currentUser.uid) .snapshots() .listen((event) { // 你的业务逻辑 }); } catch (exception) { print(exception); } }
内容的提问来源于stack exchange,提问作者zyron dias
相关产品推荐
相关产品推荐

