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?
streambuilder rebuilds with UI actions causing flicker effect
Custom Code
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.
Yes
2 replies