Streaming Firestore in Flutter
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.