Listening to a Firestore collection and its subcollections in Flutter
I´ll recommend you wrapping your widgets with two Streambuilders. Just use the first one to stream the tables and the second one to access orders inside each table. Don´t add a Streambuilder each time you want to access data. Generate the data with only two streams and pass it through variables.
Here is the exmaple code:
database.dart
import 'package:cloud_firestore/cloud_firestore.dart';
class Tables {
CollectionReference tablesReference =
FirebaseFirestore.instance.collection("tables");
Stream<QuerySnapshot> getTables() {
// Returns all tables
return tablesReference.snapshots();
}
Stream<QuerySnapshot> getOrdersFromTables(String tableName) {
// Returns specific table orders
return tablesReference.doc(tableName).collection("orders").snapshots();
}
}
main.dart
import 'package:firebase_core/firebase_core.dart';
import 'package:flutter/material.dart';
import 'tables.dart';
void main() {
runApp(MyApp());
}
class MyApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
return MaterialApp(
debugShowCheckedModeBanner: false,
theme: ThemeData.dark(),
// Initialize FlutterFire:
home: FutureBuilder(
future: Firebase.initializeApp(),
builder: (context, snapshot) {
// Check for errors
if (snapshot.hasError) {
return Center(
child: Text("Error"),
);
}
// Once complete, show your application
if (snapshot.connectionState == ConnectionState.done) {
return TablesPage();
}
// Otherwise, show something whilst waiting for initialization to complete
return Center(
child: CircularProgressIndicator(),
);
}),
);
}
}
tables.dart
import 'package:cloud_firestore/cloud_firestore.dart';
import 'package:flutter/material.dart';
import 'package:intl/intl.dart';
import 'orders.dart';
import 'database.dart';
class TablesPage extends StatefulWidget {
@override
_TablesPageState createState() => _TablesPageState();
}
class _TablesPageState extends State<TablesPage> {
@override
Widget build(BuildContext context) {
// Get Tables
return Scaffold(
appBar: AppBar(
title: Text("Tables"),
),
body: StreamBuilder<QuerySnapshot>(
stream: Tables().getTables(),
builder: (context, tables) {
if (tables.hasError)
return Text(tables.error);
else if (tables.hasData) {
if (tables.data.docs.isEmpty) {
return Text("No tables.");
} else {
return ListView.builder(
itemCount: tables.data.docs.length,
itemBuilder: (context, index) {
// Get Orders
return StreamBuilder<QuerySnapshot>(
stream: Tables()
.getOrdersFromTables(tables.data.docs[index].id),
builder: (context, orders) {
if (orders.hasData) {
return ListTile(
title:
Text("Table ${tables.data.docs[index].id}"),
subtitle: Text(DateFormat("HH:mm").format(tables
.data.docs[index]["checkinTime"]
.toDate())),
trailing: Text(
"Total orders: ${orders.data.docs.length.toString()}"),
onTap: () {
// Navigate to Orders Page
Navigator.push(
context,
MaterialPageRoute(
builder: (context) => Orders(
orders: orders.data.docs,
),
),
);
});
} else {
return Container();
}
});
},
);
}
} else {
print(tables
.connectionState); //is stuck in ConnectionState.waiting or ConnectionState.active
return Center(child: CircularProgressIndicator());
}
},
),
);
}
}
orders.dart
import 'package:cloud_firestore/cloud_firestore.dart';
import 'package:flutter/material.dart';
class Orders extends StatelessWidget {
const Orders({
Key key,
@required this.orders,
}) : super(key: key);
final List<QueryDocumentSnapshot> orders;
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text("Orders"),
),
body: ListView.builder(
itemCount: orders.length,
itemBuilder: (context, index) {
return ListTile(
title: Text(orders[index].id),
subtitle: Text(orders[index]["barStatus"]),
leading: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Text((index+1).toString()),
],
),
);
},
),
);
}
}
CodingDavid
Updated on December 26, 2022Comments
-
CodingDavid over 1 year
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 theTable
class. An instance of the Table class should have aList<Order> _orders
which contains all the orders stored in the subcollection. TheStream
should provide me with theList<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 inTable.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
-
CodingDavid over 3 yearsThanks for your help! I think this is still a bit too much code in case I want to replicate it on every screen that shows all the tables with the orders. I'll try to get a cleaner way of coding it (and wait for other responses as well), but otherwise I'll use it. Thx again :)
-
LucasACH over 3 yearsActually you dont need to add both streambuilders to each page. Just get the data once and pass it trough every class. You can use a listView.builder() for example. Using "index" you can access specific table orders. The final result would be a list of tables and when they get tapped, orders are displayed. Let me know if you want some example code.
-
CodingDavid over 3 yearsThe thing is, the data is displayed in different ways on several screens. On one screen, I need the list of all tables, so a
List<Table> tables
where every table contains its corresponding orders. On the other screen I actually only need all of the orders. For this purpose, I have usedfinal orders = tables.expand((element) => element.orders).toList()
so far. Some example code would be pretty helpful, thanks :) -
CodingDavid over 3 yearsSo I tried it out but now I can't even get all of the tables and I don't know why. The first Streambuilder is stuck with the CircularProgressIndicator and seems to stay either in ConnectionState.waiting or ConnectionState.active. Source: pastebin.com/M6NaJ2gf
-
CodingDavid over 3 yearsThanks for your support. Unfortunately I am still stuck with ConnectionState.active and simply see a CircularProgressIndicator :( Could you take a look into my source code paste?
-
CodingDavid over 3 yearsGot it working! I simply removed the snapshot.connectionState == ConnectionState.done from the if clause in my code paste and added the Firebase.initializeApp at the beginning of the app launch. Thanks for your support! <3
-
LucasACH over 3 yearsPerfect! No problem. Can you please rate my amswer, thanks!