简体   繁体   中英

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

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.

I wrote an abstraction that I am using to animate a list of tasks - it uses SliverAnimatedList but you can adapt that for a regular animated list:

// if you had a class for your list data like 
class MyItem {
  const MyItem({ required this.id, required this.name });
  final String id;
  final String name;
}


// then your build method could be like this
Widget build(BuildContext context) {

  return StreamSliverAnimatedListBuilder<MyItem>(
    stream: _loadData(), 
    build: (context, item, animation) {
      return FadeTransition(
        opacity: animation,
        child: SizeTransition(
          sizeFactor: animation,
          child: ListTile(title: Text(item.name)),
        ),
      );
    },
    compare: (p0, p1) => p0.id == p1.id
  );
}

Widget code

import 'package:flutter/widgets.dart';

class StreamSliverAnimatedListBuilder<T> extends StatefulWidget {
  const StreamSliverAnimatedListBuilder(
      {super.key,
      required this.stream,
      required this.build,
      required this.compare,
      this.fallback});

  final Stream<List<T>> stream;
  final Widget Function(
      BuildContext context, T item, Animation<double> animation) build;
  final bool Function(T, T) compare;
  final Widget? fallback;

  @override
  State<StatefulWidget> createState() {
    return _StreamSliverAnimatedListBuilderState<T>();
  }
}

class _StreamSliverAnimatedListBuilderState<T>
    extends State<StreamSliverAnimatedListBuilder<T>> {
  late final GlobalObjectKey<SliverAnimatedListState> _listKey =
      GlobalObjectKey<SliverAnimatedListState>(this);

  List<T> _currentList = [];
  bool _hasData = false;

  @override
  void initState() {
    super.initState();

    widget.stream.listen((event) {
      final List<T> newList = event;

      if (_hasData && _listKey.currentState != null) {
        List<T> addedItems = newList
            .where((a) => !_currentList.any((b) => widget.compare(a, b)))
            .toList();

        for (var update in addedItems) {
          final int updateIndex = newList.indexOf(update);
          _listKey.currentState!.insertItem(updateIndex);
        }

        List<T> removedItems = _currentList
            .where((a) => !newList.any((b) => widget.compare(a, b)))
            .toList();

        for (var update in removedItems) {
          final int updateIndex = _currentList.indexOf(update);
          _listKey.currentState!.removeItem(updateIndex, (context, animation) {
            return widget.build(context, update, animation);
          });
        }
      }

      _currentList = newList;
      if (!_hasData) {
        setState(() {
          _hasData = true;
        });
      }
    });
  }

  @override
  Widget build(BuildContext context) {
    if (!_hasData) {
      return SliverToBoxAdapter(child: widget.fallback);
    }

    return SliverAnimatedList(
      key: _listKey,
      initialItemCount: _currentList.length,
      itemBuilder: (context, index, animation) {
        if (_currentList.length <= index) {
          return Text('what. $index');
        }

        final item = _currentList[index];
        return widget.build(context, item, animation);
      },
    );
  }
}

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM