StreamBuilder builds snapshot twice for every one update

689

This is actually expected behaviour for a StreamBuilder. As you can see in this Community Answer:

StreamBuilder makes two build calls when initialized, once for the initial data and a second time for the stream data.

Streams do not guarantee that they will send data right away so an initial data value is required. Passing null to initialData throws an InvalidArgument exception.

StreamBuilders will always build twice even when the stream passed is null.

So, in order to mitigate that exception and red screen glitch, you will have to take this into consideration and treat this scenario in your code.

Share:
689
Aashit Garodia
Author by

Aashit Garodia

Updated on December 23, 2022

Comments

  • Aashit Garodia
    Aashit Garodia over 1 year

    I'm trying to build a chat application which displays time along with the message. Here is the main code:

    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),
                  ),
                ),
              ),
            ],
          ),
        );
      }
    }
    

    But I get this exception for a moment(almost a second) with a red screen and then everything works fine:

    By printing the snapshot data field values(The highlighted code in the image) for like 100 times with 100 messages, I realized that the StreamBuilder is sending updated snapshot twice.
    (You can see in the output that the first snapshot is with just time field being null and immediately in the second snapshot all values are being present, this happens for every new message I send.)
    Everything works as expected in my other app which doesn't use timestamp field in cloud firestore.

    My question is shouldn't the StreamBuilder should just send one snapshot for every one update with all the data values being present at once?
    Please tell me if I've made a mistake. Any help would be really appreciated!

  • Aashit Garodia
    Aashit Garodia over 3 years
    Thank you so much for the answer! I understand your answer but I fail to understand what happens when we don't initialize a initial data as in my case. Like isn't it weird that every time in the initial data snapshot all fields except the timestamp ( which always comes to be null) is already present ? Shouldn't all the fields be null at same time or be present at same time? If my approach of understanding is wrong please correct me!
  • Aashit Garodia
    Aashit Garodia over 3 years
    I understand that "StreamBuilder makes two build calls when initialized, once for the initial data and a second time for the stream data" BUT I don't understand how does initial data is formed? As in why eveytime in initial data: time is null, why not other fields( sender & text) becomes null even once?
  • Ralemos
    Ralemos over 3 years
    I think this is simply how the StreamBuilder was designed, the idea is that the first build cannot be guaranteed to be what you expect it to be in this scenario, which would explain why it's partially populated, so in the end the treatment in the code is still the best solution for your case. That being said, I also find it curious that part of the snapshot is correct and a specific field is not, it could be a good idea to open a tread on flutter's github to discuss this scenario.
  • Ralemos
    Ralemos over 3 years
    Also, you could try to set up initialData as an empty object to test if this changes the behaviour
  • Ray Li
    Ray Li over 3 years
    Setting initialData to an empty object does not prevent the subsequent build that is triggered when the stream is listened to.