0

I'm trying to add chat functionality to my app (kind of similar to WhatsApp functionalities), using flutter and Firestore. The main structure on Firestore is to have 2 collections (I want the unread message count as well):

  1. users: and each user will have a subcollection "chats" that will include all CHATS_IDs. This will be the main place to build home chat page (shows a history list of all chats) by getting the user chat list.
  2. chats: a list of all chats and each chat document has a subcollection of messages.

My main issue is in building the home page (where a list of all user previous chats should appear). I get/subscribe the user chat subcollection, and for each chat ID listed in there I also subscribe for the chat itself in the chat collection (using the ID).

Here are screenshots of it in principle:

users collection:

enter image description here

chats coleection:

enter image description here

and here is the main screen of interest (principle from whatsapp screen): enter image description here

What I'm doing is that I retrieve user's chat subcollection (and register a listener to it using StreamBuilder), and also for number of unread messages/last message and last message time, I subscribe to listen for each of these chats (and want to use each user last message time, status and his last presence in that chat doc to calculate the unread count) .

The problem is that Listview.builder rebuilds all items (initially and on scroll) instead of just the viewed ones. here is my code:

  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());
        });
  }

After searching, I found these related suggestions (but both didn't work):

  1. A github issue suggested thesince the stream is reordarable (github: [https://github.com/flutter/flutter/issues/58917]), but even with using ListView.custom with a delegate and a findChildIndexCallback, the same problem remained.
  2. to use distinct.

But removing the inner streambuilder and just returning the tiles without a subscription, makes the ListView.builder work as expected (only builds the viewed ones). So my questions are:

  1. Why having nested stream builders causing all items to be rebuil.
  2. is there a better structure to implement the above features (all chats with unread count and last message/time in real-time). Especially that I haven't added lazy loading yet. And also with this design, I have to update multiple documents for each message (in chats collection, and each user's subcollection).

Your help will be much appreciated (I have checked some other SO threads and medium articles, but couldn't find one that combines these features in one place with and preferably with optimized design for scalability/price using Firestore and Flutter).

M.R.M
  • 540
  • 1
  • 13
  • 30

1 Answers1

0

I think that You can do this:

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

and for _catche:

  List<Widget> _catche = [/*...*/];
  // initialize on load
  • thanks Om Patil, but const is not working with following error "Arguments of a constant creation must be constant expressions. " so I can't pass index or other params. – M.R.M Jun 08 '21 at 20:00