Flutter结合Firestore实现待发送低透明度临时聊天气泡方案咨询
实现方案
你的需求不需要替换StreamBuilder组件,只要配合本地维护的待发送消息队列就能实现,核心逻辑是点击发送后立刻在本地渲染半透的临时气泡,等Firestore同步完成后再替换为正式的全透气泡。
具体修改步骤
- 新增本地状态存储待发送消息,用来存还没同步到Firestore的消息
- 调整发送逻辑,点击发送后先把消息加入本地待发送队列立刻触发UI更新,再异步执行Firestore写入操作,写入完成后再把消息从待发送队列移除
- 合并消息列表,
StreamBuilder中渲染消息时,把本地待发送消息和Firestore返回的正式消息合并后再渲染 - 修改聊天气泡透明度规则,去掉原来按固定时长切换透明度的逻辑,改为由消息是否为待发送状态控制,更贴合实际网络情况
修改后的完整代码
import 'package:cloud_firestore/cloud_firestore.dart'; import 'package:firebase_core/firebase_core.dart'; import 'package:flutter/material.dart'; void main() async { WidgetsFlutterBinding.ensureInitialized(); await Firebase.initializeApp(); runApp( MaterialApp( home: ChatScreen(), ), ); } class ChatScreen extends StatefulWidget { @override State<StatefulWidget> createState() { return ChatScreenState(); } } class ChatScreenState extends State<ChatScreen> with TickerProviderStateMixin { final TextEditingController _textController = TextEditingController(); // 本地待发送消息队列 final List<String> _pendingMessages = []; final Stream firestoreStream = FirebaseFirestore.instance .collection("chats") .doc("david_aaron") .collection("david_aaron") .orderBy('timestamp', descending: true) .snapshots(); void _handleSubmitted(String text) async{ // 输入为空直接返回 if(text.trim().isEmpty) return; // 清空输入框 _textController.clear(); // 先把消息加入待发送队列,立刻更新UI setState(() { _pendingMessages.insert(0, text); }); try { // 异步写入Firestore,新增timestamp字段保证排序正确 await FirebaseFirestore.instance .collection("chats") .doc("david_aaron") .collection("david_aaron") .add({ "content": text, "timestamp": FieldValue.serverTimestamp() }); } catch(e) { // 这里可以增加发送失败的处理逻辑,比如保留待发送状态、显示重试按钮 debugPrint("消息发送失败:$e"); } finally { // 写入完成后从待发送队列移除消息 setState(() { _pendingMessages.remove(text); }); } } @override Widget build(BuildContext context) { return Scaffold( appBar: AppBar(title: const Text("Friendlychat")), body: Column( children: <Widget>[ Expanded( child: StreamBuilder( stream: firestoreStream, builder: (context, snapshot) { List<String> allMsgs = []; // 先加入所有待发送消息 allMsgs.addAll(_pendingMessages); if (snapshot.hasData) { QuerySnapshot doc = snapshot.data as QuerySnapshot; // 再加入Firestore返回的正式消息 for (var e in doc.docs) { Map s = e.data() as Map<String, dynamic>; allMsgs.add(s['content']); } return ListView.builder( padding: const EdgeInsets.all(8.0), reverse: true, itemBuilder: (_, int index) => ChatMessage( animate: index == 0, key: UniqueKey(), text: allMsgs[index], // 前N条是待发送消息,N为待发送队列长度 isPending: index < _pendingMessages.length, ), itemCount: allMsgs.length, ); } return const Center(child: CircularProgressIndicator()); }), ), const Divider(height: 1.0), Container( decoration: BoxDecoration(color: Theme.of(context).cardColor), child: _buildTextComposer(), ), ], ), ); } Widget _buildTextComposer() { return IconTheme( data: IconThemeData(color: Theme.of(context).colorScheme.secondary), child: Container( margin: const EdgeInsets.symmetric(horizontal: 8.0), child: Row( children: <Widget>[ Flexible( child: TextField( controller: _textController, onSubmitted: _handleSubmitted, decoration: const InputDecoration.collapsed(hintText: "Send a message"), ), ), Container( margin: const EdgeInsets.symmetric(horizontal: 4.0), child: IconButton( icon: const Icon(Icons.send), onPressed: () => _handleSubmitted(_textController.text) ), ), ], ), ), ); } } class ChatMessage extends StatefulWidget { const ChatMessage({ Key? key, required this.text, this.animate = false, // 新增是否为待发送状态参数 this.isPending = false, }) : super(key: key); final String text; final bool animate; final bool isPending; @override State<ChatMessage> createState() => _ChatMessageState(); } class _ChatMessageState extends State<ChatMessage> with TickerProviderStateMixin { late AnimationController animationController; @override void initState() { super.initState(); animationController = AnimationController( vsync: this, duration: Duration(milliseconds: widget.animate ? 800 : 0)); animationController.forward(); } @override void dispose() { animationController.dispose(); super.dispose(); } @override Widget build(BuildContext context) { return Opacity( // 待发送消息透明度0.7,正式消息透明度1 opacity: widget.isPending ? 0.7 : 1, child: SizeTransition( sizeFactor: CurvedAnimation( parent: animationController, curve: Curves.linearToEaseOut, ), axisAlignment: 0.0, child: Container( margin: const EdgeInsets.symmetric(vertical: 10.0), child: Row( crossAxisAlignment: CrossAxisAlignment.start, children: <Widget>[ Container( width: 200, height: 40, decoration: BoxDecoration( color: Colors.grey, borderRadius: BorderRadius.circular(15), ), child: Padding( padding: const EdgeInsets.only(left: 8.0, top: 8), child: Text(widget.text), ), ), ], ), ) ), ); } }
补充说明
如果需要处理重复内容的消息,可以给每条待发送消息生成唯一临时ID,匹配的时候用ID而非内容匹配,避免误删。发送失败的场景可以自行扩展,比如在待发送气泡旁增加重试、删除按钮,优化异常体验。
内容的提问来源于stack exchange,提问作者litt
相关产品推荐
相关产品推荐

