Streaming Firestore in Flutter

Lee young-jun
3 min readApr 4, 2024

--

To implement chat feature, I planned to load entire message once and listen new messages.

I chose Firestore to store messages.

Loading Firestore

Loading is very easy. Just get snapshot and transform it to message instance.

Future<Iterable<Message>> load(String chatId) async {
final messages = getMessages(chatId)!;
final snapshot = await messages.get();
return snapshot.docs.map((doc) => MessageFactory.fromRemote(doc));
}

Streaming

I have current entire messages now. Next, I need to get new message. Maybe I could to get regularly new messages with query by createdAt.

However I wanted to get new messages immediately as they created. Fortunately Firestore offers stream for query and the snapshots have the reason why the document changed. There is a DocumentChangeType added thus I guessed it will return only new messages.

.snapshots()
.where((snapshot) => snapshot.docChanges
.any((docChange) => docChange.type == DocumentChangeType.added))
.map((snapshot) => snapshot.docChanges.map((docChange) => MessageFactory.fromRemote(docChange.doc));

StreamController

When I stream messages before, I just linked a view model’s property to a stream method of the database service. However I need to append new messages and remove messages if needed.

How to merge the load and the stream?

We can make a custom stream with StreamController.

final messageStreamController =
StreamController<Iterable<Message>>.broadcast();

loadMessages() {
final loadedMessages = await _messageLoadService.load(chatId);
_storedMessages = List.from(loadedMessages);
messageStreamController.add(_storedMessages);
}

startListeningMessages() async {
_messageLoadService.streamMessages(chatId).listen((newMessages) {
_storedMessages.addAll(newMessages);
messageStreamController.add(_storedMessages);
});
}

The stream is probably unnecessary, but I implemented view with StreamBuilder.

Modified

I launched the app and saw duplicated messages.

According to the official guide, This is because First snapshot contains added events of all exist documents.

So I switched the document change type to modified. And the problem has been disappeared.

.where((snapshot) => snapshot.docChanges
.any((docChange) => docChange.type == DocumentChangeType.modified))

Timestamp is null

I expected the stream won’t emit new messages if I send a new message. Therefore I thought that I should filter with change type and createdAt

However when I removed the where , my app was crashed.

Why? Because createdAt was null. Actually added event has null createdAt but the stream modified event also.

This is caused by Server Timestamp. I used FieldValue.serverTimestamp() to set value of createdAt by server side.

So I restored thewhere and everything is fine!

If you found this post helpful, please give it a round of applause 👏. Explore more Flutter-related content in my other posts.

For additional insights and updates, check out my LinkedIn profile. Thank you for your support!

Troubleshooting

type ‘Null’ is not a subtype of type ‘Timestamp’ in type cast

set default timestamp during parsing or receive only modified event.

References

--

--

No responses yet