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

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.09.26 05:06:07