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:

  1. It stores the current messages in a list currentMessageList outside the build method
  2. It listens to the stream to get new messages and compares the new list with the previous one in currentMessageList.
  3. It gets the difference between both lists and loops through to update the AnimatedList widget at the specific index updateIndex.

The Message code above does the following:

  • It overrides the == operator and the object hashcode 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 different Message objects with the same values would not be equivalent].
  • It uses the equatable package to avoid boiler-plate.
Share:
365
Tobias Svendsen
Author by

Tobias Svendsen

Updated on December 22, 2022

Comments

  • Tobias Svendsen
    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
    Tobias Svendsen almost 3 years
    Thanks! 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
    Victor Eronmosele almost 3 years
    When 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
    Tobias Svendsen almost 3 years
    I 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
    Victor Eronmosele almost 3 years
    Do you have this line : currentMessageList = messageList; in your initState in the stream listener?
  • Tobias Svendsen
    Tobias Svendsen almost 3 years
    Yes, 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
    Victor Eronmosele almost 3 years
    Okay. Can you print messageList and updateList at the line just before this if statement: ` if (_listKey.currentState != null && _listKey.currentState.widget.initialItemCount < messageList.length)` and post the result?
  • Tobias Svendsen
    Tobias Svendsen almost 3 years
    I 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
    Victor Eronmosele almost 3 years
    I think the equality check for the Message object for the updateList is failing to identify similar objects. You'll need to override the equality operator. Please update your question with your Message class.
  • Tobias Svendsen
    Tobias Svendsen almost 3 years
    I 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
    Victor Eronmosele almost 3 years
    Yeah 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
    Victor Eronmosele almost 3 years
    I've added the update. Please, check it out @TobiasSvendsen
  • Tobias Svendsen
    Tobias Svendsen almost 3 years
    Thanks! 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
    Victor Eronmosele almost 3 years
  • John
    John almost 3 years
    This code helped me very much, but I also need to animate on removing object from a list. How to do this?
  • Victor Eronmosele
    Victor Eronmosele almost 3 years
    HI @John, please open a new question with your current code and post the link here.