如何在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); });
补充说明
- 需要修改
ItemPageState的构造函数,添加List<String>? varieties参数,用来接收配料列表,然后在详情页用单选按钮组件渲染这些选项。 - 废弃原来的
getOptions函数和count变量,因为已经不需要通过计数来匹配餐品ID了。 - 第一种方式性能更好,一次性从数据库拉取所有数据;第二种方式适合配料数据量较大的场景,减少初始加载的数据量。
内容的提问来源于stack exchange,提问作者Stamp777

