The result I try to achieve
I use the Firestore server for the backend of a Flutter application for an order system of a restaurant. I found myself in kind of a challenge when implementing a screen that shows real time changes of the database.
The structure in the Firestore DB is as follows:

So I've got a collection called 'tables' which contains several documents (which might have a different name for different restaurants). Each of those documents has several fields, e.g. 'checkinTime'. Additionally, every table document also has a collection of orders ("the subcollection").
I want to get a Stream on my Flutter screen which receives all of the tables, including their orders. The screen should give an overview of all tables, that means it is supposed to update whenever...
- a new table document was created
- a table document was updated
- a new order was placed into a tables subcollection 'orders'
- an order document was updated
I have a model of the Order and the Table class. An instance of the Table class should have a List<Order> _orders which contains all the orders stored in the subcollection. The Stream should provide me with the List<Table>.
Note: I try to achieve this with Flutter Web, but that probably doesn't make a difference regarding the solution of my problem.
Current progress
I actually managed to almost solve the challenge so far.
I've managed to get a Stream of all the tables onto the screen.
In most of the cases, it initially loads the data successfully. Sometimes the first load of the screen results in an infinite loading CircularProgressIndicator and the data is not loaded.
The screen updates, when another Table is added to the collection. Adding an order doesn't work immediately, but if I reload the page a second time or after I add another order to the order subcollection it works. It seems like the error occurs independent of the timing.
It seems like updating an order document doesn't work/reload the order state at all.
There is no error message in any case.
Current code
Class TableInfo for the Stream:
import 'package:cloud_firestore/cloud_firestore.dart';
import 'package:flutter/material.dart' as m;
import '../../../../../domain/infrastructure/firestore_infrastructure.dart';
import '../../../../../domain/models/orders_models/order.dart';
import '../../../../../domain/models/orders_models/table.dart';
import '../../../../../locator.dart';
///Class for getting all tables and orders in realtime and managing the current filter settings.
class TableInfo extends m.ChangeNotifier {
///List of all tables
List<Table> _tables = [];
///List of all orders
List<Order> _orders = [];
final CollectionReference _tableCollection = locator<CustomFirestore>().tablesRef;
Stream<QuerySnapshot> get _stream => _tableCollection.snapshots();
///returns all tables in the Firestore `tables` collection of the restaurant.
///Also sets a listener for the orders and matches them from the [_orders] list.
Stream<List<Table>> getTableStream(
{void Function(Order savedOrder) onOrderSaved}) async* {
print("Reloading");
final tableDocs = await _tableCollection.get().then((value) => value.docs);
for (QueryDocumentSnapshot tableDoc in tableDocs) {
_tableCollection
.doc(tableDoc.id)
.collection(CustomFirestore.ORDERS_COLLECTION)
.snapshots()
.listen((snapshot) =>
_saveOrders(snapshot, tableDoc.id, onOrderSaved: onOrderSaved));
}
final tableStream =
_stream.map((snapShot) => _unfilteredTablesFromTableSnapshot(snapShot));
yield* tableStream;
}
///Help function to get all tables with ordes from a table doc snapshot.
List<Table> _unfilteredTablesFromTableSnapshot(QuerySnapshot snapShot) {
return snapShot.docs.map((tableDocument) {
final table = Table.fromSnapshot(tableDocument);
table.addOrders(_orders.where((element) => element.tableId == table.id),
overwrite: true);
_tables.add(table);
return table;
}).toList();
}
///Function to save the orders from a snapshot of an order collection of a single table.
void _saveOrders(QuerySnapshot orderCollectionSnapshot, String tableId,
{void Function(Order savedOrder) onOrderSaved}) {
final orderDocs = orderCollectionSnapshot.docs;
print(
"Saving ${orderDocs.length} orders for $tableId at ${DateTime.now()} :)");
for (QueryDocumentSnapshot orderDoc in orderDocs) {
final order = Order.fromSnapshot(orderDoc, tableId);
final existing = _orders.firstWhere(
(element) => element.orderId == order.orderId,
orElse: () => null);
if (existing != null) _orders.remove(existing);
_orders.add(order);
if (onOrderSaved != null) onOrderSaved(order);
}
}
}
As you can see, I first get all of the table documents and then add a listener for the subcollection 'orders', called _saveOrders. It seems like the code gets the orders first, that's why I initialize the Table in Table.fromSnapshot with _orders = [] and then add the orders from the order list within the _unfilteredTablesFromTableSnapshot method.
Here is the relevant part of the screens build method (placed within a Scaffold, obviously):
final tableInfo = Provider.of<TableInfo>(context);
return StreamProvider.value(
value: tableInfo.getTableStream(),
catchError: (ctx, error) {
print("Error occured! $error");
print(error.runtimeType);
//throw error;
return [
t.Table(
id: 'fehler',
name: 'Fehler $error',
status: t.TableStatus.Free,
orders: [],
checkinTime: DateTime.now())
];
},
builder: (context, child) {
final allUnfilteredTables = Provider.of<
List<t.Table>>(
context); //corresponds to TableInfo.filteredTableStream
///while the Tables are loading:
if (allUnfilteredTables == null)
return Center(
child: CircularProgressIndicator());
///if there are no tables at all:
if (allUnfilteredTables.isEmpty)
return Text(
"No tables detected.");
///if an error occured:
if (allUnfilteredTables[0].id == 'fehler')
return Text(allUnfilteredTables[0].name);
return GridView.builder(
gridDelegate:
SliverGridDelegateWithFixedCrossAxisCount(
crossAxisSpacing: (50 / 1920) *
constraints.maxWidth,
crossAxisCount: isTablet ? 3 : 6),
itemCount: allUnfilteredTables.length,
itemBuilder: (ctx, index) {
t.Table table = allUnfilteredTables[index];
return TableMiniDisplay(table);
},
);
});
What I've already tried
I've read a lot about related issues but couldn't find any answer that helped me that much in order to fix my remaining errors. I also tried using rxdart and using a CombinedStream but couldn't get it running.
The major part of the real time synchronization works, so I guess there's only little holding me back from success and I thank everyone of you who took your time in order to read about my problem. I appreciate any ideas or code samples that could help me.
( Also if you got any other advice to improve the code, feel free to leave a comment :) )
Thank you in advance!
Cheers, David
