Aashit Garodia
Aashit Garodia

Reputation: 410

StreamBuilder builds snapshot twice for every one update

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!

Upvotes: 2

Views: 922

Answers (1)

Rafael Lemos
Rafael Lemos

Reputation: 5829

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.

Upvotes: 3

Related Questions