How can I mix a statenotifier, with streamproviders that fetch 2 collections from Firebase to get constant update on a chat list

Viewed 142

This is my first question here and I hope I’m not making it too complex.

So, I’m a junior programmer and I’ve start learning flutter, firebase and riverpod a couple of months ago and I’m stuck in a specific project.

For part of the app, what I need to do is something very similar to WhatsApp:

  1. A screen with all the user chats,
  2. This screen (or part of it) should update every time a chat gets a new message, showing the last message snippet and turning into bold if not read,
  3. The shown chats should change between unarchived and archived, depending on a button in the UI.

Couldn’t have picked up an easier starting project, right? ;) (now every time I look at WhatsApp I say wow!)

Regarding firebase/firestore I’m fetching 2 different collections for this:

  • the sub-collection ‘chats’ within the ‘user_chats’ collection: where I get all the chat Ids plus it’s status (if this chat is archived and if the last message was read), for the current user,
  • the main ‘chats’ collection, where I have the main info of each chat.

At this moment I’m doing this:

In the chats_screen (UI) I’m fetching my chat provider: userChatsProvider (note: this is just an example of the UI implementation. I have another implementation for it, but as long I get the chatName and lastMsgContent updated for each chat, perfect.)

class ChatsScreen extends ConsumerWidget {
  const ChatsScreen({Key? key}) : super(key: key);

  @override
  Widget build(BuildContext context, WidgetRef ref) {
   (…)
    return Scaffold(
      appBar: (…) // not relevant for this question
      body: Center(
        child: Consumer(
          builder: (BuildContext context, WidgetRef ref, Widget? child) {
            return ref.watch(userChatsProvider).when(
                  loading: () => const CircularProgressIndicator(),
                  error: (err, st) => Center(child: Text(err.toString())),
                  data: (chatData) {
                    return Column(
                      children: [
                        // button to get archived chats
                          child: TextButton(
                              child: (…)
                              onPressed: () {}),
                        ),
                        Expanded( 
                          child: ListView.builder(
                            itemCount: chatData.length,
                            itemBuilder: (ctx, index) => Row( 
                               children: [
                                  Text (chatData[index].chatName!),
                                  Text (chatData[index].lastMsgContent!),
                               ]
                            ),
                          ),
                        ),
                      ]
                    );
                  }
             );
           }
         )
       )
     );
  }
}

In the chats_provider (provider) I’m fetching the 2 repository providers and joining them into a specific model I’ve created for this screen:

final userChatsProvider = FutureProvider.autoDispose<List<ChatScreenModel>>((ref) async {
  const String userId = ‘XXX’; // this will be substituted by a dynamic variable with the current user Id

  try {
    final List<UserChat> userChats =
        await ref.watch(userChatsRepositoryProvider).get(userId: userId);

    final List<String> chatIdList = userChats.map<String>((e) => e.id).toList();
    final List<Chat> chats =
        await ref.watch(chatsRepositoryProvider).get(chatIds: chatIdList);

    // ref.maintainState = true;
    return toChatScreenModel(chats, userChats);
  } on Exception catch (e) {
    throw const CustomException();
  }
});

In the repositories I’m fetching the firestorm collections I mentioned above. Here’s an example:

final userChatsRepositoryProvider =
    Provider<UserChatsRepository>((ref) => UserChatsRepository(ref.read));

class UserChatsRepository {
  final Reader _read;

  const UserChatsRepository(this._read);

  Future<List<UserChat>> get({required String userId}) async {

    try {
      final snap = await _read(firebaseFirestoreProvider)
          .collection('users_chats/$userId/chats')
          .get();
    
      // Maybe this step is not necessary, but I’ve decided to transform the data into temporary models, before sending to provider
      List<UserChat> userChats =
          snap.docs.map((doc) => UserChat.fromJson(doc.data(), doc.id)).toList();

      return userChats;
    } on FirebaseException catch (e) {
      throw CustomException(message: e.message);
    }
  }
}

And by the way, this is the model I’m sending to the UI:

class ChatScreenModel {
  final String? id;
  final String? chatName;
  final String? chatImage;
  final String? lastMsgContent;
  final String? lastMsgDate;
  final ChatType? chatType;
  final bool? archived;
  final bool? msgRead;

  ChatScreenModel({
    this.id,
    this.chatName,
    this.chatImage,
    this.lastMsgContent,
    this.lastMsgDate,
    this.chatType,
    this.archived,
    this.msgRead,
  });

Problems with this implementation:

  1. I’m getting the user chats in the screen, but they don’t update since I’m not using a stream. So I get a snapshot, but it will only update if I leave and enter that chats_screen again. And it would be important to have it updating with a stream.
  2. I’m showing all the chats, and not a filtered list with only the unarchived chats.
  3. Also, related with the previous point, I still don’t have the archived button working, to only show the archived chats.

I’ve lost many, many hours trying to understand how I could implement a stream provider and a state notifier provider in this workflow. Tried many combinations, but without success.

Can anyone help me understand how to do this?

Priority: transform these providers into stream providers (so it updates the UI constantly). Nice to have: also include the archived/unarchived dynamic to filter the chats that appear and be able to switch between them.

Thanks a lot. :)

0 Answers
Related