在 Flutter 中监听 Firestore 集合及其子集合

Cod*_*vid 5 dart firebase flutter google-cloud-firestore flutter-layout

我试图达到的结果

我使用 Firestore 服务器作为餐厅订单系统 Flutter 应用程序的后端。我发现自己在实现显示数据库实时更改的屏幕时遇到了挑战。

Firestore DB中的结构如下: Firestore DB 中的结构

所以我有一个名为“tables”的集合,其中包含多个文档(不同的餐厅可能有不同的名称)。每个文档都有几个字段,例如“checkinTime”。此外,每个表文档还具有订单集合(“子集合”)。

我想Stream在我的 Flutter 屏幕上看到一个接收所有表格的屏幕,包括他们的订单。屏幕应该给出所有表格的概述,这意味着它应该在任何时候更新......

  • 创建了一个新的表格文档
  • 更新了表格文档
  • 新订单被放入表子集合“orders”中
  • 订单文件已更新

Order我有和类的模型Table。Table 类的实例应该有一个List<Order> _orders包含子集合中存储的所有订单的 。应该Stream为我提供List<Table>.

注意:我尝试使用 Flutter Web 来实现这一点,但这对于解决我的问题可能没有什么影响。

现在的进展

到目前为止,我实际上已经几乎解决了这个挑战。我已经设法将Stream所有表格显示到屏幕上。

在大多数情况下,它最初会成功加载数据。有时,第一次加载屏幕会导致无限加载 CircularProgressIndicator 并且未加载数据。

当另一个表添加到集合中时,屏幕会更新。添加订单不会立即起作用,但如果我再次重新加载页面或在将另一个订单添加到订单子集合后,它就会起作用。似乎错误的发生与时间无关。

更新订单文档似乎根本不起作用/重新加载订单状态。

任何情况下都不会出现错误消息。

当前代码

TableInfo流类:

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);
    }
  }
}
Run Code Online (Sandbox Code Playgroud)

正如您所看到的,我首先获取所有表文档,然后为子集合“orders”添加一个名为 的侦听器_saveOrders。看起来代码首先获取订单,这就是为什么我使用Table.fromSnapshot_orders = [] 初始化表,然后从方法内的订单列表添加订单_unfilteredTablesFromTableSnapshot

这是 screenbuild方法的相关部分(显然,放置在脚手架中):

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);
                                    },
                                  );
                                });
Run Code Online (Sandbox Code Playgroud)

我已经尝试过的

我已经阅读了很多相关问题,但找不到任何对我有很大帮助的答案来解决我剩余的错误。我还尝试使用rxdart和 CombinedStream 但无法运行。

实时同步的主要部分是有效的,所以我想几乎没有什么阻碍我成功,我感谢所有花时间阅读我的问题的人。我感谢任何可以帮助我的想法或代码示例。

(此外,如果您有任何其他改进代码的建议,请随时发表评论:))

先感谢您!

干杯,大卫

Luc*_*ACH 5

I\xc2\xb4ll 建议您使用两个Streambuilder包装您的小部件。只需使用第一个来流式传输表,使用第二个来访问每个表内的订单。不要\xc2\xb4t 每次要访问数据时都添加 Streambuilder。仅使用两个流生成数据并将其传递给变量。

\n

在此输入图像描述

\n

这是示例代码:

\n

数据库.dart

\n
import \'package:cloud_firestore/cloud_firestore.dart\';\n\nclass Tables {\n  CollectionReference tablesReference =\n      FirebaseFirestore.instance.collection("tables");\n\n  Stream<QuerySnapshot> getTables() {\n    // Returns all tables\n    return tablesReference.snapshots();\n  }\n\n  Stream<QuerySnapshot> getOrdersFromTables(String tableName) {\n    // Returns specific table orders\n    return tablesReference.doc(tableName).collection("orders").snapshots();\n  }\n}\n
Run Code Online (Sandbox Code Playgroud)\n

主程序.dart

\n
import \'package:firebase_core/firebase_core.dart\';\nimport \'package:flutter/material.dart\';\nimport \'tables.dart\';\n\n\nvoid main() {\n  runApp(MyApp());\n}\n\nclass MyApp extends StatelessWidget {\n  @override\n  Widget build(BuildContext context) {\n    return MaterialApp(\n      debugShowCheckedModeBanner: false,\n      theme: ThemeData.dark(),\n      // Initialize FlutterFire:\n      home: FutureBuilder(\n          future: Firebase.initializeApp(),\n          builder: (context, snapshot) {\n            // Check for errors\n            if (snapshot.hasError) {\n              return Center(\n                child: Text("Error"),\n              );\n            }\n\n            // Once complete, show your application\n            if (snapshot.connectionState == ConnectionState.done) {\n              return TablesPage();\n            }\n\n            // Otherwise, show something whilst waiting for initialization to complete\n            return Center(\n              child: CircularProgressIndicator(),\n            );\n          }),\n    );\n  }\n}\n
Run Code Online (Sandbox Code Playgroud)\n

表.dart

\n
import \'package:cloud_firestore/cloud_firestore.dart\';\nimport \'package:flutter/material.dart\';\nimport \'package:intl/intl.dart\';\nimport \'orders.dart\';\nimport \'database.dart\';\n\nclass TablesPage extends StatefulWidget {\n  @override\n  _TablesPageState createState() => _TablesPageState();\n}\n\nclass _TablesPageState extends State<TablesPage> {\n  @override\n  Widget build(BuildContext context) {\n    // Get Tables\n    return Scaffold(\n      appBar: AppBar(\n        title: Text("Tables"),\n      ),\n      body: StreamBuilder<QuerySnapshot>(\n        stream: Tables().getTables(),\n        builder: (context, tables) {\n          if (tables.hasError)\n            return Text(tables.error);\n          else if (tables.hasData) {\n            if (tables.data.docs.isEmpty) {\n              return Text("No tables.");\n            } else {\n              return ListView.builder(\n                itemCount: tables.data.docs.length,\n                itemBuilder: (context, index) {\n                  // Get Orders\n                  return StreamBuilder<QuerySnapshot>(\n                      stream: Tables()\n                          .getOrdersFromTables(tables.data.docs[index].id),\n                      builder: (context, orders) {\n                        if (orders.hasData) {\n                          return ListTile(\n                              title:\n                                  Text("Table ${tables.data.docs[index].id}"),\n                              subtitle: Text(DateFormat("HH:mm").format(tables\n                                  .data.docs[index]["checkinTime"]\n                                  .toDate())),\n                              trailing: Text(\n                                  "Total orders: ${orders.data.docs.length.toString()}"),\n                              onTap: () {\n                                // Navigate to Orders Page\n                                Navigator.push(\n                                  context,\n                                  MaterialPageRoute(\n                                    builder: (context) => Orders(\n                                      orders: orders.data.docs,\n                                    ),\n                                  ),\n                                );\n                              });\n                        } else {\n                          return Container();\n                        }\n                      });\n                },\n              );\n            }\n          } else {\n            print(tables\n                .connectionState); //is stuck in ConnectionState.waiting or ConnectionState.active\n            return Center(child: CircularProgressIndicator());\n          }\n        },\n      ),\n    );\n  }\n}\n
Run Code Online (Sandbox Code Playgroud)\n

订单.dart

\n
import \'package:cloud_firestore/cloud_firestore.dart\';\nimport \'package:flutter/material.dart\';\n\nclass Orders extends StatelessWidget {\n  const Orders({\n    Key key,\n    @required this.orders,\n  }) : super(key: key);\n\n  final List<QueryDocumentSnapshot> orders;\n\n  @override\n  Widget build(BuildContext context) {\n    return Scaffold(\n      appBar: AppBar(\n        title: Text("Orders"),\n      ),\n      body: ListView.builder(\n        itemCount: orders.length,\n        itemBuilder: (context, index) {\n          return ListTile(\n            title: Text(orders[index].id),\n            subtitle: Text(orders[index]["barStatus"]),\n            leading: Column(\n              mainAxisAlignment: MainAxisAlignment.center,\n              children: [\n                Text((index+1).toString()),\n              ],\n            ),\n          );\n        },\n      ),\n    );\n  }\n}\n
Run Code Online (Sandbox Code Playgroud)\n