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

Posted

技术标签:

【中文标题】在 Flutter 中监听 Firestore 集合及其子集合【英文标题】:Listening to a Firestore collection and its subcollections in Flutter 【发布时间】:2021-03-10 16:26:01 【问题描述】:

我试图达到的结果

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

Firestore DB 中的结构如下:

所以我有一个名为“tables”的集合,其中包含多个文档(对于不同的餐厅可能有不同的名称)。这些文档中的每一个都有几个字段,例如'登记时间'。此外,每个表格文档还有一个订单集合(“子集合”)。

我想在我的 Flutter 屏幕上获得一个Stream,它接收所有表格,包括他们的订单。屏幕应该提供所有表格的概览,这意味着它应该随时更新......

创建了一个新的表格文档 表格文档已更新 一个新订单被放置到一个表格子集合“订单”中 订单文档已更新

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

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

当前进度

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

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

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

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

在任何情况下都没有错误消息。

当前代码

TableInfo 的 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);
    
  

如您所见,我首先获取所有表格文档,然后为子集合“orders”添加一个侦听器,称为_saveOrders。似乎代码首先获取订单,这就是为什么我使用 _orders = [] 初始化 Table.fromSnapshot 中的表,然后在 _unfilteredTablesFromTableSnapshot 方法中添加订单列表中的订单。

这是屏幕build 方法的相关部分(显然是放置在脚手架内):

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);
                                    ,
                                  );
                                );

我已经尝试过的

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

实时同步的主要部分有效,所以我想没有什么能阻止我成功,我感谢你们花时间阅读我的问题的每一个人。我感谢任何可以帮助我的想法或代码示例。

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

提前谢谢你!

干杯,大卫

【问题讨论】:

【参考方案1】:

我建议您使用两个 Streambuilders 来包装您的小部件。只需使用第一个流式传输表格,第二个访问每个表格内的订单。不要在每次要访问数据时添加 Streambuilder。仅使用两个流生成数据并通过变量传递。

示例代码如下:

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();
  

ma​​in.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()),
              ],
            ),
          );
        ,
      ),
    );
  

【讨论】:

感谢您的帮助!我认为这仍然有点太多代码,以防我想在每个显示所有订单表的屏幕上复制它。我将尝试以一种更简洁的方式对其进行编码(并等待其他响应),否则我将使用它。再次感谢:) 实际上您不需要将两个流构建器添加到每个页面。只需获取一次数据并将其传递给每个班级。例如,您可以使用 listView.builder()。使用“索引”,您可以访问特定的表订单。最终结果将是一个表格列表,当它们被点击时,就会显示订单。如果您需要一些示例代码,请告诉我。 问题是,数据在几个屏幕上以不同的方式显示。在一个屏幕上,我需要所有表的列表,因此需要List&lt;Table&gt; tables,其中每个表都包含其相应的订单。在另一个屏幕上,我实际上只需要所有订单。为此,到目前为止,我使用了final orders = tables.expand((element) =&gt; element.orders).toList()。一些示例代码会很有帮助,谢谢:) 所以我尝试了一下,但现在我什至无法获得所有表格,我不知道为什么。第一个 Streambuilder 被 CircularProgressIndicator 卡住,并且似乎停留在 ConnectionState.waiting 或 ConnectionState.active 中。来源:pastebin.com/M6NaJ2gf 搞定了!我只是从我的代码粘贴中的 if 子句中删除了 snapshot.connectionState == ConnectionState.done,并在应用程序启动开始时添加了 Firebase.initializeApp。感谢您的支持!

以上是关于在 Flutter 中监听 Firestore 集合及其子集合的主要内容,如果未能解决你的问题,请参考以下文章

如何使用异步函数异步监听 Firestore 中的值?

Firestore 单文档快照流未更新

如何在 Flutter 中为 Firebase 连接设置监听器?

Flutter:Streambuilder - 关闭流

Flutter - 从 Stream 接收然后修改数据

Firestore 集合查询作为颤动中的流