You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

如何在Flutter中从Firebase RTDB获取餐品配料等子元素

问题描述

我的menuItem数据结构如下:

menuItem
        J1
           -description:"Tasty milk shake!"
           -img:"assets/images/milkshake.JPG"
           -itemName:"Milk Shake"
           -price:20
           -varieties
                    -var1:"Chocolate"
                    -var2:"Vanilla"
                    -var3:"Strawberry"

我用StreamBuilder从Firebase Realtime Database的每条记录里拉取信息生成菜单列表,用户点击菜单条目时会跳转到商品详情页查看信息并加入购物车。现在需要给部分餐品添加配料选项,让用户在详情页通过单选按钮选择。

目前我通过统计StreamBuilder生成的列表长度,给每个计数加J生成ID来获取对应餐品的配料,这种方式虽然能拿到配料值,但没法把配料和对应的餐品条目关联起来。

我的StreamBuilder代码:

StreamBuilder(
            
            stream: _dbRef.child('menuItem').onValue,
            builder: (context, snapshot) {
              final tilesList =
                  <InkWell>[]; 

              if (snapshot.hasData) {
                final myRewards = Map<String, dynamic>.from(
                    (snapshot.data! as DatabaseEvent).snapshot.value
                        as Map<dynamic, dynamic>);

                myRewards.forEach((key, value) {
                  final nextReward = Map<String, dynamic>.from(
                      value); 

                  final price = nextReward['price'];
                  String itemName = nextReward['itemName'];
                  String desc = nextReward['description'];
                  String itemImg = nextReward['img'];

                  final rewardTile = InkWell(
                      onTap: () async {
                        var pls = options;

                        Navigator.of(context).push(MaterialPageRoute(
                            builder: (context) => ItemPageState(
                                  itemName: itemName,
                                  desc: desc,
                                  price: price,
                                  itemImg: itemImg,
                                )));
                        // }
                      },
                      child: Card(
                        margin: const EdgeInsets.fromLTRB(8, 10, 8, 10),
                        color: Colors.white,
                        clipBehavior: Clip.antiAlias,
                        shape: RoundedRectangleBorder(
                          borderRadius: BorderRadius.circular(24),
                        ),
                        child: Row(
                            crossAxisAlignment: CrossAxisAlignment.center,
                            children: [
                              ClipRect(
                                  child: Align(
                                alignment: Alignment.center,
                                widthFactor: 0.8,
                                child: Image(
                                  image: AssetImage(itemImg),
                                  height: 100,
                                  fit: BoxFit.cover,
                                ),
                              )),
                              const SizedBox(width: 30),
                              Column(
                                mainAxisAlignment: MainAxisAlignment.start,
                                crossAxisAlignment: CrossAxisAlignment.start,
                                children: [
                                  Text(
                                    itemName,
                                    style: const TextStyle(
                                        color: Colors.black, fontSize: 25),
                                  ),
                                  Text(
                                    'Price: R' + price.toString(),
                                    style: const TextStyle(
                                        color: Colors.black, fontSize: 18),
                                  ),
                                ],
                              ),
                              const SizedBox(width: 30),
                              
                            ]),
                      ));

                  tilesList.add(rewardTile);
                  count = tilesList.length;
                 
                });
                getOptions(count);
              }

我的getOptions代码:

Future<void> getOptions(int count) async {
      for (var itemNum = 1; itemNum <= count; itemNum++) {
        String strItemNum = itemNum.toString();
        String itemID = 'J$strItemNum';

        final hope = await _dbRef.child('menuItem/$itemID/varieties').get();
        if (hope.exists) {
          options.add(hope.value.toString());
        }
      }
    }

请问有没有可行的实现方法?我的思路是不是错了?


解决方案

你的思路存在明显问题:通过列表计数生成J+数字的ID匹配餐品,完全依赖条目在列表中的顺序,一旦数据库里的条目顺序变化(比如新增、删除餐品),就会出现配料和餐品不匹配的情况;而且getOptions是遍历完所有条目后批量获取,没法实时对应到单个餐品。

正确的做法是在遍历每个餐品条目时,直接从当前条目的数据里获取配料,或者利用当前餐品的key(也就是J1、J2这类ID)去单独拉取配料,这样就能保证配料和餐品一一对应。

具体有两种实现方式:

方式一:直接从当前条目数据提取varieties

如果数据库返回的value里已经包含varieties字段(从你的数据结构看是有的),根本不需要额外调用getOptions,直接在遍历的时候拿:

myRewards.forEach((key, value) {
  final nextReward = Map<String, dynamic>.from(value); 

  final price = nextReward['price'];
  String itemName = nextReward['itemName'];
  String desc = nextReward['description'];
  String itemImg = nextReward['img'];
  // 直接获取当前餐品的配料
  List<String>? varieties;
  if (nextReward.containsKey('varieties')) {
    final vars = Map<String, dynamic>.from(nextReward['varieties']);
    varieties = vars.values.cast<String>().toList();
  }

  final rewardTile = InkWell(
      onTap: () {
        Navigator.of(context).push(MaterialPageRoute(
            builder: (context) => ItemPageState(
                  itemName: itemName,
                  desc: desc,
                  price: price,
                  itemImg: itemImg,
                  varieties: varieties, // 把配料传递到详情页
                )));
      },
      // 后面的Card代码保持不变...
  );

  tilesList.add(rewardTile);
});

方式二:利用当前餐品的key单独拉取配料(适合varieties数据量大的情况)

如果varieties数据较多,不想一次性全部加载,可以在用户点击餐品时,用当前餐品的key(也就是myRewards.forEach里的key参数,比如J1)实时获取配料,再传递到详情页:

myRewards.forEach((key, value) {
  final nextReward = Map<String, dynamic>.from(value); 

  final price = nextReward['price'];
  String itemName = nextReward['itemName'];
  String desc = nextReward['description'];
  String itemImg = nextReward['img'];

  final rewardTile = InkWell(
      onTap: () async {
        List<String>? varieties;
        // 用当前餐品的key直接查询varieties
        final snapshot = await _dbRef.child('menuItem/$key/varieties').get();
        if (snapshot.exists) {
          final vars = Map<String, dynamic>.from(snapshot.value as Map);
          varieties = vars.values.cast<String>().toList();
        }
        Navigator.of(context).push(MaterialPageRoute(
            builder: (context) => ItemPageState(
                  itemName: itemName,
                  desc: desc,
                  price: price,
                  itemImg: itemImg,
                  varieties: varieties,
                )));
      },
      // 后面的Card代码保持不变...
  );

  tilesList.add(rewardTile);
});

补充说明

  1. 需要修改ItemPageState的构造函数,添加List<String>? varieties参数,用来接收配料列表,然后在详情页用单选按钮组件渲染这些选项。
  2. 废弃原来的getOptions函数和count变量,因为已经不需要通过计数来匹配餐品ID了。
  3. 第一种方式性能更好,一次性从数据库拉取所有数据;第二种方式适合配料数据量较大的场景,减少初始加载的数据量。

内容的提问来源于stack exchange,提问作者Stamp777

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.08.24 14:54:14