streambuilder rebuilds with UI actions causing flicker effect

Custom Code

I have a streambuilder widget which listens to and displays a chat conversation. when the user sends a new message from the UI, the widget rebuilds to display the new message causing a flickering effect, which is obviously a bad user experience. How can I fix this?

What have you tried so far?

Below is my original implementation of the streambuilder widget.

class DisplayRealTimeMessages2 extends StatefulWidget {
  const DisplayRealTimeMessages2({
    Key? key,
    required this.width,
    required this.height,
    required this.userId,
  }) : super(key: key);

  final double width;
  final double height;
  final String userId;

  @override
  _DisplayRealTimeMessages2State createState() =>
      _DisplayRealTimeMessages2State();
}

class _DisplayRealTimeMessages2State extends State<DisplayRealTimeMessages2> {
  @override
  Widget build(BuildContext context) {
    return Container(
      width: widget.width,
      height: widget.height,
      child: StreamBuilder<DocumentSnapshot>(
        stream: FirebaseFirestore.instance
            .collection('users')
            .doc(widget.userId)
            .snapshots(),
        builder: (context, snapshot) {
          if (snapshot.hasError) {
            return Text('Error: ${snapshot.error}');
          }
          if (snapshot.connectionState == ConnectionState.waiting) {
            return Center(child: CircularProgressIndicator());
          }

          var userDoc = snapshot.data?.data() as Map<String, dynamic>;
          var chatRef = userDoc['current_chat'];
          if (chatRef == null) {
            return Text('No chat selected.');
          }

          String chatId = chatRef.id;
          return StreamBuilder<QuerySnapshot>(
            stream: FirebaseFirestore.instance
                .collection('chats/$chatId/chat_messages')
                .orderBy('timestamp', descending: true)
                .snapshots(),
            builder: (context, chatSnapshot) {
              if (chatSnapshot.hasError) {
                return Text('Error: ${chatSnapshot.error}');
              }
              if (chatSnapshot.connectionState == ConnectionState.waiting) {
                return Center(child: CircularProgressIndicator());
              }

              final messages = chatSnapshot.data?.docs ?? [];
              return ListView.builder(
                itemCount: messages.length,
                reverse: true,
                itemBuilder: (context, index) {
                  final messageData =
                      messages[index].data() as Map<String, dynamic>;
                  final bool isCurrentUser =
                      messageData['sender'] == widget.userId;
                  final DateTime messageTime =
                      (messageData['timestamp'] as Timestamp).toDate();
                  final String displayTime =
                      DateFormat.jm().format(messageTime);

                  return Align(
                    alignment: isCurrentUser
                        ? Alignment.centerLeft
                        : Alignment.centerRight,
                    child: Column(
                      crossAxisAlignment: isCurrentUser
                          ? CrossAxisAlignment.start
                          : CrossAxisAlignment.end,
                      children: [
                        Text(
                          messageData['sender'],
                          style: TextStyle(
                              color: isCurrentUser
                                  ? Color(0xFFFFAFF9)
                                  : Color(0xFFFFE266),
                              fontSize: 16),
                        ),
                        CustomPaint(
                          painter: ChatBubble(
                            color: Colors.black.withOpacity(0.3),
                            alignment: isCurrentUser
                                ? Alignment.topLeft
                                : Alignment.topRight,
                          ),
                          child: Container(
                            padding: EdgeInsets.all(10),
                            child: Text(
                              messageData['content'],
                              style:
                                  TextStyle(color: Colors.white, fontSize: 14),
                            ),
                          ),
                        ),
                        Text(
                          displayTime,
                          style:
                              TextStyle(color: Color(0xFFC8C7C7), fontSize: 10),
                        ),
                      ],
                    ),
                  );
                },
              );
            },
          );
        },
      ),
    );
  }
}



I have tried using the advice here: https://stackoverflow.com/questions/63006297/flutter-streambuilder-removes-old-data-while-loading and tried initializing my firestore stream as a variable during initState instead of fetching the data directly in the streambuilder stream property and modified my above implementation as follows:

class DisplayRealTimeMessages extends StatefulWidget {
  const DisplayRealTimeMessages({
    super.key,
    this.width,
    this.height,
    required this.userId,
  });

  final double? width;
  final double? height;
  final String userId;

  @override
  State<DisplayRealTimeMessages> createState() => _DisplayRealTimeMessagesState();
}

class _DisplayRealTimeMessagesState extends State<DisplayRealTimeMessages> {
  late final Stream<DocumentSnapshot> userStream;
  Stream<QuerySnapshot>? chatMessagesStream;

  @override
  void initState() {
    super.initState();
    userStream = FirebaseFirestore.instance
        .collection('users')
        .doc(widget.userId)
        .snapshots();

    userStream.listen((userSnapshot) {
      final userDoc = userSnapshot.data() as Map<String, dynamic>?;
      final chatRef = userDoc?['current_chat'];
      if (chatRef != null) {
        // Setup chat messages listener
        chatMessagesStream = FirebaseFirestore.instance
            .collection('chats/${chatRef.id}/chat_messages')
            .orderBy('timestamp', descending: true)
            .snapshots();
        setState(() {});
      }
    });
  }

  @override
  Widget build(BuildContext context) {
    return Container(
      width: widget.width,
      height: widget.height,
      child: StreamBuilder<DocumentSnapshot>(
        stream: userStream,
        builder: (context, snapshot) {
          if (snapshot.connectionState == ConnectionState.waiting) {
            return Center(child: CircularProgressIndicator());
          }

          if (snapshot.hasError) {
            return Text("Error: ${snapshot.error}");
          }

          var userDoc = snapshot.data?.data() as Map<String, dynamic>?;
          var otherUserRef = userDoc?['current_chat_other_user'];
          return FutureBuilder<DocumentSnapshot>(
            future: otherUserRef?.get(),
            builder: (context, otherUserSnapshot) {
              if (!otherUserSnapshot.hasData) {
                return Text("Waiting for a match...");
              }

              var otherUserDoc = otherUserSnapshot.data?.data() as Map<String, dynamic>?;
              var displayName = otherUserDoc?['display_name'] ?? 'Unknown User';

              return Column(
                children: [
                  Text("You are connected with $displayName"),
                  Expanded(
                    child: chatMessagesStream == null
                        ? Center(child: Text("No messages yet."))
                        : StreamBuilder<QuerySnapshot>(
                            stream: chatMessagesStream,
                            builder: (context, chatSnapshot) {
                              if (chatSnapshot.connectionState == ConnectionState.waiting) {
                                return Center(child: CircularProgressIndicator());
                              }

                              final messages = chatSnapshot.data?.docs ?? [];
                              return ListView.builder(
                                itemCount: messages.length,
                                reverse: true,
                                itemBuilder: (context, index) {
                                  final messageData = messages[index].data() as Map<String, dynamic>;
                                  return ListTile(
                                    title: Text(messageData['sender']),
                                    subtitle: Text(messageData['content']),
                                  );
                                },
                              );
                            },
                          ),
                  ),
                ],
              );
            },
          );
        },
      ),
    );
  }
}

However this has not resolved the issue.

Did you check FlutterFlow's Documentation for this topic?
Yes
2 replies