Flutter + Firestore 聊天 .. Listview 重建所有项目

问题描述

我正在尝试使用 Flutter 和 Firestore 向我的应用添加聊天功能(有点类似于 WhatsApp 功能)。 Firestore 的主要结构是有 2 个集合(我也想要未读消息数):

  1. users:每个用户都有一个包含所有 CHATS_ID 的子集合“聊天”。这将是通过获取用户聊天列表来构建主页聊天页面显示所有聊天的历史列表)的主要位置。
  2. 聊天:所有聊天的列表,每个聊天文档都有一个消息子集。

我的主要问题是构建主页(应显示所有用户以前聊天的列表)。我获取/订阅用户聊天子集合,并且对于其中列出的每个聊天 ID,我还订阅聊天集合中的聊天本身(使用 ID)。

以下是原理截图:

用户集合:

enter image description here

聊天收集:

enter image description here

这是感兴趣的主屏幕(来自whatsapp屏幕的原理):

enter image description here

我正在做的是检索用户的聊天子集合(并使用 StreamBuilder 为其注册一个侦听器),并且对于未读消息数/最后一条消息和最后一条消息时间,我订阅了这些聊天中的每一个(并希望使用每个用户的最后一条消息时间、状态和他最后一次出现在该聊天文档中来计算未读计数)。

问题在于 Listview.builder 重建所有项目(初始和滚动时),而不仅仅是查看的项目。这是我的代码

  Stream<QuerySnapshot> getCurrentUserChats(userId) {
    return FirebaseFirestore.instance
        .collection(AppConstants.USERS_COLLECTION)
        .doc('$userId')
        .collection(AppConstants.USER_CHATS_SUBCOLLECTION)
        .orderBy('lastMsgTS',descending: true)
        .snapshots()
        .distinct();
  }

  Widget getRecentChats(userId) {
    return StreamBuilder<QuerySnapshot>(
        stream: getCurrentUserChats(userId),builder: (context,snapshot) {
          if (snapshot.hasData && snapshot.data.docs.isNotEmpty) {
            print('snapshot of user chats subcoll has changed');
            List<QueryDocumentSnapshot> retrievedDocs = snapshot.data.docs;
            return Container(
              height: 400,child: ListView.builder(
                //childrenDelegate: SliverChildBuilderDelegate(
                itemCount: snapshot.data.size,itemBuilder: (context,index) {
                  String chatId = retrievedDocs[index].id;
                  print('building index: $index,chatId: $chatId');

                  return StreamBuilder(
                    stream: FirebaseFirestore.instance
                        .collection(AppConstants.CHATS_COLLECTION)
                        .doc('$chatId')
                        .snapshots()
                        .distinct(),builder:
                        (context,AsyncSnapshot<DocumentSnapshot> snapshot) {

                      if (snapshot.hasData) {
                        print('${snapshot.data?.id},isExist: ${snapshot.data?.exists}');
                        if (snapshot.data.exists) {
                          return KeyProxy(
                            key: ValueKey(chatId),child: ListTile(
                              leading: CircleAvatar(
                                child: Container(
                                  //to be replaced with user image
                                  color: Colors.red,),title: Text('$chatId'),subtitle: Text(
                                  "Last Message received on: ${DateTimeUtils.getDateViewFromDT(snapshot.data.data()['ts']?.toDate())}"),);
                        }
                      }

                      return SizedBox.shrink();
                    },);
                },/*childCount: snapshot.data.size,findChildindexCallback: (Key key) {
                        print('calling findChildindexCallback');
                        final ValueKey valKey = key;
                        final String docId = valKey.value;
                        int idx = retrievedDocs.indexOf(retrievedDocs
                            .where((element) => element.id == docId)
                            .toList()[0]);
                        print('docId: $docId,idx: $idx');
                        return idx;
                      }*/
              ),);
          }

          return Center(child: UIWidgetUtils.loader());
        });
  }

搜索后,我找到了这些相关的建议(但都不起作用):

  1. 一个 github 问题建议,因为流是可重新排序的(github:[https://github.com/Flutter/Flutter/issues/58917]),但即使使用带有委托和 findChildindexCallback 的 ListView.custom,也一样问题仍然存在。
  2. 使用distinct。

但是删除内部流构建器并在没有订阅的情况下仅返回图块,会使 ListView.builder 按预期工作(仅构建查看的图块)。所以我的问题是:

  1. 为什么嵌套流构建器会导致重新构建所有项目。
  2. 是否有更好的结构来实现上述功能(所有带有未读计数和最后一条消息/时间的实时聊天)。特别是我还没有添加延迟加载。此外,使用这种设计,我必须为每条消息更新多个文档(在聊天集合和每个用户的子集合中)。

您的帮助将不胜感激(我已经查看了一些其他 SO 主题和中等文章,但找不到将这些功能与使用 Firestore 和 Flutter 的可扩展性/价格优化设计结合在一处的文章)。

解决方法

我认为你可以这样做:

  Widget build(ctx) {
    return ListView.builder(
      itemCount: snapshot.data.size,itemBuilder: (index,ctx) =>_catche[index],)
  }

对于_catch:

  List<Widget> _catche = [/*...*/];
  // initialize on load