StreamBuilder 为每次更新构建两次快照

Posted

技术标签:

【中文标题】StreamBuilder 为每次更新构建两次快照【英文标题】:StreamBuilder builds snapshot twice for every one update 【发布时间】:2020-12-21 01:08:11 【问题描述】:

我正在尝试构建一个显示时间和消息的聊天应用程序。以下是主要代码:

import 'package:flutter/material.dart';
import 'package:flash_chat/constants.dart';
import 'package:firebase_auth/firebase_auth.dart';
import 'package:cloud_firestore/cloud_firestore.dart';

final _fireStore = Firestore.instance;
FirebaseUser loggedInUser;

class ChatScreen extends StatefulWidget 
  static String chatScreen = 'ChatScreenpage1';
  @override
  _ChatScreenState createState() => _ChatScreenState();


class _ChatScreenState extends State<ChatScreen> 
  final messageTextEditingController = TextEditingController();
  String messageText;

  final _auth = FirebaseAuth.instance;

  @override
  void initState() 
    super.initState();
    getUserDetail();
  

  void getUserDetail() async 
    try 
      final createdUser = await _auth.currentUser();
      if (createdUser != null) 
        loggedInUser = createdUser;
      
     catch (e) 
      print(e);
    
  

  @override
  Widget build(BuildContext context) 
    return Scaffold(
      appBar: AppBar(
        leading: null,
        actions: <Widget>[
          IconButton(
              icon: Icon(Icons.close),
              onPressed: () 

                _auth.signOut();
                Navigator.pop(context);
              ),
        ],
        title: Text('⚡️Chat'),
        backgroundColor: Colors.lightBlueAccent,
      ),
      body: SafeArea(
        child: Column(
          mainAxisAlignment: MainAxisAlignment.spaceBetween,
          crossAxisAlignment: CrossAxisAlignment.stretch,
          children: <Widget>[
            StreambuilderClass(),
            Container(
              decoration: kMessageContainerDecoration,
              child: Row(
                crossAxisAlignment: CrossAxisAlignment.center,
                children: <Widget>[
                  Expanded(
                    child: TextField(
                      controller: messageTextEditingController,
                      onChanged: (value) 
                        messageText = value;
                      ,
                      decoration: kMessageTextFieldDecoration,
                    ),
                  ),
                  FlatButton(
                    onPressed: () 
                      messageTextEditingController.clear();
                      _fireStore.collection('messages').add(
                        'sender': loggedInUser.email,
                        'text': messageText,
                        'time': FieldValue.serverTimestamp()
                      );
                    ,
                    child: Text(
                      'Send',
                      style: kSendButtonTextStyle,
                    ),
                  ),
                ],
              ),
            ),
          ],
        ),
      ),
    );
  


class StreambuilderClass extends StatelessWidget 
  @override
  Widget build(BuildContext context) 
    return StreamBuilder<QuerySnapshot>(
        stream: _fireStore
            .collection('messages')
            .orderBy('time', descending: false)
            .snapshots(),
        builder: (context, snapshot) 
          if (!snapshot.hasData) 
            return Center(
              child: CircularProgressIndicator(
                backgroundColor: Colors.blueAccent,
              ),
            );
          
          final messages = snapshot.data.documents.reversed;
          List<MessageBubble> messageBubbles = [];
          for (var message in messages) 
            final messageText = message.data['text'];
            final messageSender = message.data['sender'];
            final messageTime = message.data['time'] as Timestamp;
            final currentUser = loggedInUser.email;

          print('check time: $messageTime'); //print(message.data['time']); both gives null
          print('check sender: $messageSender');
          print('check sender: $messageText');
          print(snapshot.connectionState);

            final messageBubble = MessageBubble(
              sender: messageSender,
              text: messageText,
              isMe: currentUser == messageSender,
              time: messageTime,
            );

            messageBubbles.add(messageBubble);
          

          return Expanded(
            child: ListView(
                reverse: true,
                padding: EdgeInsets.symmetric(horizontal: 10, vertical: 20),
                children: messageBubbles),
          );
        );
  


class MessageBubble extends StatelessWidget 
  final String text;
  final String sender;
  final bool isMe;
  final Timestamp time;

  MessageBubble(this.text, this.sender, this.isMe, this.time); 
  @override
  Widget build(BuildContext context) 
    return Padding(
      padding: EdgeInsets.all(10.0),
      child: Column(
        crossAxisAlignment:
            isMe ? CrossAxisAlignment.end : CrossAxisAlignment.start,
        children: <Widget>[
          Text(
            ' $sender $DateTime.fromMillisecondsSinceEpoch(time.seconds * 1000)',
            style: TextStyle(color: Colors.black54, fontSize: 12),
          ),
          Material(
            color: isMe ? Colors.blueAccent : Colors.white,
            borderRadius: isMe
                ? BorderRadius.only(
                    topLeft: Radius.circular(30),
                    bottomLeft: Radius.circular(30),
                    bottomRight: Radius.circular(30))
                : BorderRadius.only(
                    topRight: Radius.circular(30),
                    bottomLeft: Radius.circular(30),
                    bottomRight: Radius.circular(30)),
            elevation: 6,
            child: Padding(
              padding: EdgeInsets.symmetric(horizontal: 20, vertical: 15),
              child: Text(
                text,
                style: TextStyle(
                    fontSize: 20, color: isMe ? Colors.white : Colors.black),
              ),
            ),
          ),
        ],
      ),
    );
  

但我得到了这个异常片刻(几乎一秒钟)的红屏,然后一切正常: 通过使用 100 条消息打印快照数据字段值(图像中突出显示的代码)大约 100 次,我意识到 StreamBuilder 正在发送两次更新的快照。 (您可以在输出中看到第一个快照只有 时间字段为空,而在第二个快照中,所有值都立即出现,这发生在我发送的每条新消息中。) 在我的另一个应用程序中,一切都按预期工作,它不使用云 Firestore 中的时间戳字段。

我的问题是 StreamBuilder 不应该只为每次更新发送一个快照,同时所有数据值都同时存在吗? 如果我犯了错误,请告诉我。任何帮助将不胜感激!

【问题讨论】:

【参考方案1】:

这实际上是 StreamBuilder 的预期行为。正如您在Community Answer 中看到的那样:

StreamBuilder 在初始化时会进行两次构建调用,一次用于 初始数据和流数据的第二次。

流不保证它们会立即发送数据,因此 初始数据值是必需的。将 null 传递给 initialData 会引发 InvalidArgument 异常。

StreamBuilders 将始终构建两次,即使传递的流是 空。

因此,为了减轻该异常和红屏故障,您必须考虑到这一点并在代码中处理这种情况。

【讨论】:

非常感谢您的回答!我理解你的回答,但我不明白当我们不像我的情况那样初始化初始数据时会发生什么。每次在初始数据快照中,除了时间戳(总是为空)之外的所有字段都已经存在,这不是很奇怪吗?不应该所有字段同时为空或同时存在吗?如果我的理解方法有误请指正! 我了解“StreamBuilder 在初始化时会进行两次构建调用,一次用于初始数据,第二次用于流数据”但我不明白初始数据是如何形成的?至于为什么初始数据中的eveytime:时间为空,为什么其他字段(发件人和文本)甚至一次都为空? 我认为这只是 StreamBuilder 的设计方式,其想法是不能保证第一个构建是您在这种情况下所期望的,这可以解释为什么它是部分填充的,所以最后,代码中的处理仍然是您案例的最佳解决方案。话虽如此,我也很奇怪部分快照是正确的,而某个特定字段不正确,最好在flutter's github 上讨论这种情况。 另外,您可以尝试将initialData 设置为空对象来测试这是否会改变行为 initialData设置为空对象并不会阻止监听流时触发的后续构建。

以上是关于StreamBuilder 为每次更新构建两次快照的主要内容,如果未能解决你的问题,请参考以下文章

Streambuilder 渲染两次,第一次使用初始数据,第二次使用正确数据,但小部件不更新

Flutter:将firestore快照转换为streambuilder中的列表

尽管快照中有数据,Streambuilder snapshot.hasData 永远不会为真

来自一个 StreamBuilder 的 Flutter 快照显示在另一个 StreamBuilder 中

Flutter:StreamBuilder 快照——无数据

尽管通过 Stream 发送,StreamBuilder 的快照没有数据