简体   繁体   English

在 StreamBuilder 中使用 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.我正在使用 firebase 构建聊天应用程序,目前我将每条消息作为文档存储在 firebase 中的集合中。我使用 StreamBuilder 获取最新消息并显示它们。 I want to add an animation when a new message is received and sent.我想在收到和发送新消息时添加一个 animation。 I have tried using an Animatedlist, however, I don't get how to make it work with a StreamBuilder.我尝试过使用 Animatedlist,但是,我不知道如何让它与 StreamBuilder 一起工作。 As far as I understand I would have to call the insertItem function each time a new message is added.据我了解,每次添加新消息时我都必须调用insertItem function。 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:您可以将小部件的State更新为以下内容:

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:此外,将您的Message class 更新为以下代码:

// 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:上面的State代码执行以下操作:

  1. It stores the current messages in a list currentMessageList outside the build method它将当前消息存储在 build 方法之外的列表currentMessageList
  2. It listens to the stream to get new messages and compares the new list with the previous one in currentMessageList .它侦听 stream 以获取新消息,并将新列表与currentMessageList中的前一个列表进行比较。
  3. It gets the difference between both lists and loops through to update the AnimatedList widget at the specific index updateIndex .它通过在特定索引updateIndex处更新AnimatedList小部件来获取列表和循环之间的差异。

The Message code above does the following:上面的Message代码执行以下操作:

  • 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();它覆盖==运算符和 object hashcode码以允许在此行中进行检查: 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]. [如果不覆盖这些 getter,检查将失败,因为具有相同值的两个不同Message对象将不等价]。
  • It uses the equatable package to avoid boiler-plate.它使用相等的package来避免样板。

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:我写了一个抽象,我用它来制作任务列表的动画 - 它使用 SliverAnimatedList 但你可以将它改编为常规动画列表:

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

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

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