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 永远不会为真