Search code examples
fluttergoogle-cloud-firestorestream-builderflutter-animatedlist

Use AnimatedList inside a StreamBuilder


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,
          )
        ],
      ),
    );
  }
}```

Solution

  • 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.