使用Riverpod代码生成:Firestore消息列表本地预更新实现方案
实现思路
- 采用AsyncNotifier(配合Riverpod代码生成)管理消息状态,状态存储包含本地临时消息的完整列表,通过标记区分临时消息与已同步至Firestore的正式消息
- 发送消息流程:先创建带临时ID的本地消息并加入状态,即时更新UI;再异步调用Firestore写入接口,成功后替换临时消息为带真实ID的正式消息,失败则移除临时消息并处理错误
- 监听Firestore集合的实时快照,同步其他设备发送的消息,同时避免重复添加已存在于本地的临时消息
示例代码
1. 定义消息模型
class Message { final String id; final String content; final String senderId; final DateTime timestamp; final bool isLocalTemp; // 标记是否为本地临时消息 Message({ required this.id, required this.content, required this.senderId, required this.timestamp, this.isLocalTemp = false, }); // 从Firestore文档转模型 factory Message.fromFirestore(Map<String, dynamic> data, String docId) { return Message( id: docId, content: data['content'], senderId: data['senderId'], timestamp: (data['timestamp'] as Timestamp).toDate(), ); } // 转Firestore可存储的Map Map<String, dynamic> toFirestore() { return { 'content': content, 'senderId': senderId, 'timestamp': Timestamp.fromDate(timestamp), }; } // 复制方法,用于替换临时消息为正式消息 Message copyWith({ String? id, String? content, String? senderId, DateTime? timestamp, bool? isLocalTemp, }) { return Message( id: id ?? this.id, content: content ?? this.content, senderId: senderId ?? this.senderId, timestamp: timestamp ?? this.timestamp, isLocalTemp: isLocalTemp ?? this.isLocalTemp, ); } }
2. 实现Riverpod AsyncNotifier
import 'package:riverpod_annotation/riverpod_annotation.dart'; import 'package:cloud_firestore/cloud_firestore.dart'; part 'messages_provider.g.dart'; @riverpod class MessagesNotifier extends _$MessagesNotifier { // Firestore集合引用 final CollectionReference _messagesRef = FirebaseFirestore.instance.collection('messages'); @override Future<List<Message>> build() async { // 监听Firestore实时快照,同步消息 return _messagesRef.orderBy('timestamp').snapshots().map((snapshot) { final remoteMessages = snapshot.docs.map((doc) => Message.fromFirestore(doc.data() as Map<String, dynamic>, doc.id)).toList(); // 合并本地临时消息与远程消息,过滤已同步的临时消息 if (state.hasValue) { final localTempMessages = state.value!.where((msg) => msg.isLocalTemp).toList(); final filteredRemote = remoteMessages.where((remoteMsg) { return !localTempMessages.any((tempMsg) => tempMsg.content == remoteMsg.content && tempMsg.senderId == remoteMsg.senderId && tempMsg.timestamp.difference(remoteMsg.timestamp).inSeconds.abs() < 5 ); }).toList(); return [...localTempMessages, ...filteredRemote]; } return remoteMessages; }).first; } Future<void> sendMessage({required String content, required String senderId}) async { // 1. 创建本地临时消息,用时间戳生成临时ID final tempId = 'temp_${DateTime.now().millisecondsSinceEpoch}'; final tempMessage = Message( id: tempId, content: content, senderId: senderId, timestamp: DateTime.now(), isLocalTemp: true, ); // 2. 立即更新状态,加入临时消息 state = AsyncValue.data([...state.valueOrNull ?? [], tempMessage]); try { // 3. 写入Firestore final docRef = await _messagesRef.add(tempMessage.toFirestore()); // 4. 替换临时消息为带真实ID的正式消息 final syncedMessage = tempMessage.copyWith( id: docRef.id, isLocalTemp: false, ); state = AsyncValue.data( state.value!.map((msg) => msg.id == tempId ? syncedMessage : msg).toList() ); } catch (e) { // 5. 写入失败,移除临时消息 state = AsyncValue.data( state.value!.where((msg) => msg.id != tempId).toList() ); rethrow; } } }
3. UI层使用示例
import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; class ChatScreen extends ConsumerWidget { final TextEditingController _messageController = TextEditingController(); final String currentUserId = 'user_123'; // 当前用户ID,实际项目中从登录态获取 ChatScreen({super.key}); @override Widget build(BuildContext context, WidgetRef ref) { final messagesAsync = ref.watch(messagesProvider); return Scaffold( appBar: AppBar(title: const Text('聊天')), body: Column( children: [ Expanded( child: messagesAsync.when( loading: () => const Center(child: CircularProgressIndicator()), error: (err, stack) => Center(child: Text('加载失败: $err')), data: (messages) { return ListView.builder( reverse: true, itemCount: messages.length, itemBuilder: (context, index) { final msg = messages[index]; return Align( alignment: msg.senderId == currentUserId ? Alignment.centerRight : Alignment.centerLeft, child: Container( padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 8), margin: const EdgeInsets.symmetric(horizontal: 8, vertical: 4), decoration: BoxDecoration( color: msg.isLocalTemp ? Colors.grey[200] : msg.senderId == currentUserId ? Colors.blue[100] : Colors.grey[100], borderRadius: BorderRadius.circular(16), ), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text(msg.content), if (msg.isLocalTemp) const SizedBox(height: 4), const Text( '发送中...', style: TextStyle(fontSize: 12, color: Colors.grey), ), ], ), ), ); }, ); }, ), ), Padding( padding: const EdgeInsets.all(8.0), child: Row( children: [ Expanded( child: TextField( controller: _messageController, decoration: const InputDecoration( hintText: '输入消息...', border: OutlineInputBorder( borderRadius: BorderRadius.all(Radius.circular(24)), ), contentPadding: EdgeInsets.symmetric(horizontal: 16, vertical: 8), ), ), ), const SizedBox(width: 8), CircleAvatar( child: IconButton( icon: const Icon(Icons.send), onPressed: () async { final content = _messageController.text.trim(); if (content.isNotEmpty) { try { await ref.read(messagesProvider.notifier).sendMessage( content: content, senderId: currentUserId, ); _messageController.clear(); } catch (e) { ScaffoldMessenger.of(context).showSnackBar( SnackBar(content: Text('发送失败: $e')), ); } } }, ), ), ], ), ), ], ), ); } }
内容的提问来源于stack exchange,提问作者Ayan Das
相关产品推荐
相关产品推荐

