Use AnimatedList inside a StreamBuilder
365
You can update your widget's State
to this below:
class _MessagesWidgetState extends State<MessagesWidget> {
final GlobalKey<AnimatedListState> _listKey = GlobalKey<AnimatedListState>();
Tween<Offset> _offset = Tween(begin: Offset(1, 0), end: Offset(0, 0));
Stream<List<Message>> stream;
List<Message> currentMessageList = [];
User user;
@override
void initState() {
super.initState();
user = Provider.of<User>(context, listen: false);
stream = DatabaseService(uid: user.uid).getMessages(widget.receiver);
stream.listen((newMessages) {
final List<Message> messageList = newMessages;
if (_listKey.currentState != null &&
_listKey.currentState.widget.initialItemCount < messageList.length) {
List<Message> updateList =
messageList.where((e) => !currentMessageList.contains(e)).toList();
for (var update in updateList) {
final int updateIndex = messageList.indexOf(update);
_listKey.currentState.insertItem(updateIndex);
}
}
currentMessageList = messageList;
});
}
@override
Widget build(BuildContext context) {
return Container(
child: Column(
crossAxisAlignment: CrossAxisAlignment.center,
mainAxisSize: MainAxisSize.max,
mainAxisAlignment: MainAxisAlignment.end,
children: <Widget>[
Expanded(
child: StreamBuilder<List<Message>>(
stream: stream,
builder: (context, snapshot) {
switch (snapshot.connectionState) {
case ConnectionState.waiting:
return Loading();
default:
final messages = snapshot.data;
return messages.isEmpty
? SayHi(
userID: widget.receiver,
)
: AnimatedList(
key: _listKey,
physics: BouncingScrollPhysics(),
reverse: true,
initialItemCount: messages.length,
itemBuilder: (context, index, animation) {
final message = messages[index];
return SlideTransition(
position: animation.drive(_offset),
child: MessageWidget(
message: message,
userID: widget.receiver,
isCurrentUser: message.uid == user.uid,
),
);
},
);
}
}),
),
SizedBox(
height: 10,
),
NewMessage(
receiver: widget.receiver,
)
],
),
);
}
}
Also, update your Message
class to the code below:
// Using the equatable package, remember to add it to your pubspec.yaml file
import 'package:equatable/equatable.dart';
class Message extends Equatable{
final String uid;
final String message;
final Timestamp timestamp;
Message({this.uid, this.timestamp, this.message});
@override
List<Object> get props => [uid, message, timestamp];
}
Explanation:
The State
code above does the following:
- It stores the current messages in a list
currentMessageList
outside the build method - It listens to the stream to get new messages and compares the new list with the previous one in
currentMessageList
. - It gets the difference between both lists and loops through to update the
AnimatedList
widget at the specific indexupdateIndex
.
The Message
code above does the following:
- It overrides the
==
operator and the objecthashcode
to allow the check in this line:List<Message> updateList = messageList.where((e) => !currentMessageList.contains(e)).toList();
work as intended. [Without overriding these getters, the check would fail as two differentMessage
objects with the same values would not be equivalent]. - It uses the equatable package to avoid boiler-plate.
Author by
Tobias Svendsen
Updated on December 22, 2022Comments
-
Tobias Svendsen over 1 year
I am building a chat app with firebase and I am currently storing each message as a document inside a collection in firebase. I use a StreamBuilder to get the latest messages and display them. I want to add an animation when a new message is received and sent. I have tried using an Animatedlist, however, I don't get how to make it work with a StreamBuilder. As far as I understand I would have to call the insertItem function each time a new message is added. Is there a smarter way to do it? Or how would this be implemented?
This is what I have so far:
class Message { final String uid; final String message; final Timestamp timestamp; Message({this.uid, this.timestamp, this.message}); } class MessagesWidget extends StatefulWidget { final String receiver; MessagesWidget({@required this.receiver}); @override _MessagesWidgetState createState() => _MessagesWidgetState(); } class _MessagesWidgetState extends State<MessagesWidget>{ final GlobalKey<AnimatedListState> _listKey = GlobalKey<AnimatedListState>(); Tween<Offset> _offset = Tween(begin: Offset(1,0), end: Offset(0,0)); @override Widget build(BuildContext context) { final user = Provider.of<User>(context); return Container( child: Column( crossAxisAlignment: CrossAxisAlignment.center, mainAxisSize: MainAxisSize.max, mainAxisAlignment: MainAxisAlignment.end, children: <Widget>[ Expanded( child: StreamBuilder<List<Message>>( stream: DatabaseService(uid: user.uid).getMessages(widget.receiver), builder: (context, snapshot) { switch (snapshot.connectionState) { case ConnectionState.waiting: return Loading(); default: final messages = snapshot.data; return messages.isEmpty ? SayHi(userID: widget.receiver,) : AnimatedList( key: _listKey, physics: BouncingScrollPhysics(), reverse: true, initialItemCount: messages.length, itemBuilder: (context, index, animation) { final message = messages[index]; return SlideTransition( position: animation.drive(_offset), child: MessageWidget( message: message, userID: widget.receiver, isCurrentUser: message.uid == user.uid, ), ); }, ); } }), ), SizedBox( height: 10, ), NewMessage( receiver: widget.receiver, ) ], ), ); } }```
-
Tobias Svendsen almost 3 yearsThanks! That approach seems to work. However, for some reason, the whole list is animated instead of just a single new message. Would you happen to have any idea how to change that? Really appreciate your detailed answer.
-
Victor Eronmosele almost 3 yearsWhen you say "the whole list is animated", does this refer to when the screen opens for the first time or when you add a new document to the collection? Also, please include a screen recording as that'll help with debugging this.
-
Tobias Svendsen almost 3 yearsI meant to say that the whole list of messages are animated, not just the one being added. It's because of this line of code:
List<Message> updateList = (messageList.where((e) => !currentMessageList.contains(e))).toList();
The updateList always contains all the elements of the stream which means that they get inserted to the animated list and therefore all elements are animated. I don't know how to insert a video, however, it basically just makes the whole list animate when a new message I added. -
Victor Eronmosele almost 3 yearsDo you have this line :
currentMessageList = messageList;
in your initState in the stream listener? -
Tobias Svendsen almost 3 yearsYes, I do. I copied the code you sent directly only changing the two occurrences of 'List<String>' to also be 'List<Message>' otherwise the code is identical. Printing the length of updateList and also printing the updateIndex inside the update loop gives: 26,0,1...25 which means that it updates the whole list.
-
Victor Eronmosele almost 3 yearsOkay. Can you print
messageList
andupdateList
at the line just before this if statement: ` if (_listKey.currentState != null && _listKey.currentState.widget.initialItemCount < messageList.length)` and post the result? -
Tobias Svendsen almost 3 yearsI can't print the updateList before its declared. However printing the mesageList before the if-statement results in this: [Instance of 'Message', ... , Instance of 'Message'] and if I print the updateList right after it is declared (inside the if-statement) I get the same thing except that messageList.length = 26 and updatedList.length = 27.
-
Victor Eronmosele almost 3 yearsI think the equality check for the
Message
object for theupdateList
is failing to identify similar objects. You'll need to override the equality operator. Please update your question with yourMessage
class. -
Tobias Svendsen almost 3 yearsI think you are right. I have now updated it. I could alternatively add an id to the message class that is the document id of the firestore document. The uid that is currently associated is the userid of the person that sent the message.
-
Victor Eronmosele almost 3 yearsYeah you can add a message-id using the document id. I'll update my answer with an update to the Message class shortly.
-
Victor Eronmosele almost 3 yearsI've added the update. Please, check it out @TobiasSvendsen
-
Tobias Svendsen almost 3 yearsThanks! It works as intended now. Didn't know dart required you to go through such hoops to make custom objects comparable. Really appreciate your help!
-
Victor Eronmosele almost 3 yearsGlad to help! You can check these documentation links for more information on this: api.dart.dev/stable/2.13.3/dart-core/Object/…, dart.dev/guides/libraries/library-tour#implementing-map-keys, api.dart.dev/stable/2.13.3/dart-core/Object/hashCode.html. :)
-
John almost 3 yearsThis code helped me very much, but I also need to animate on removing object from a list. How to do this?
-
Victor Eronmosele almost 3 yearsHI @John, please open a new question with your current code and post the link here.