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

使用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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.06.27 14:37:56