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

Flutter接入Firebase Database时数据重复添加无法更新的修复求助

问题根因
  • Firebase Realtime Database的onValue监听器会在对应节点数据每次变动时触发全量回调,原有代码每次回调都直接向本地列表执行add操作,没有清空旧数据,导致每次数据更新都会重复累加条目
  • 未存储每个产品对应的Firebase节点唯一key,后续更新操作无法定位到需要修改的具体节点,只能触发新增逻辑
  • 缺少更新数据的交互与提交逻辑,原有浮动按钮点击事件为空,没有对接Firebase的更新接口
  • 原有列表空判断逻辑productNames != []永远返回true,空数据场景下会引发索引异常
调整方案
  • 每次监听器触发回调时,先清空本地所有存储产品数据的列表,再添加新拉取的全量数据,避免重复
  • 新增productKeys列表存储每个产品对应的Firebase节点key,更新操作时通过该key定位目标节点
  • 给列表卡片增加点击交互,点击后弹出编辑窗口输入新的产品数值,调用Firebase的update方法提交更新
  • 修正列表空判断逻辑,直接使用列表长度判断即可
修正后完整代码
import 'package:flutter/material.dart';
import 'package:firebase_database/firebase_database.dart';
// 请确保你已经导入了FontAwesomeIcons相关依赖
import 'package:font_awesome_flutter/font_awesome_flutter.dart';

// 你原有项目的自定义样式和组件,保持不变
const kBlackTS = TextStyle(color: Colors.black);
Widget emptySpaceWidth(BuildContext context, double val) => SizedBox(width: MediaQuery.of(context).size.width * val);
Widget emptySpaceHeight(BuildContext context, double val) => SizedBox(height: MediaQuery.of(context).size.height * val);
class CustomAppBar {
  final BuildContext context;
  final Color colour;
  final String title;
  CustomAppBar({required this.context, required this.colour, required this.title});
  AppBar get customAppBar => AppBar(backgroundColor: colour, title: Text(title));
}

class DetailedCountingScreen extends StatefulWidget {
  final String countDate;
  final String standName;
  const DetailedCountingScreen(
    {required this.countDate, required this.standName});

  @override
  _DetailedCountingScreenState createState() => _DetailedCountingScreenState();
}

class _DetailedCountingScreenState extends State<DetailedCountingScreen> {
  final firebaseRef = FirebaseDatabase.instance.reference();
  // 新增存储产品节点key的列表
  List<String> productKeys = [];
  List<String> productNames = [];
  List<int> soldPieces = [];
  List<int> countedPieces = [];
  List<double> productPrices = [];

  @override
  void initState() {
    // 修正:super.initState要放在initState最前面
    super.initState();
    firebaseRef
      .child('Sayımlar')
      .child(widget.standName)
      .child('${widget.countDate}')
      .onValue
      .listen((event) {
        final snapshot = event.snapshot.value ?? {};
        final countedProductsMap = Map<String, dynamic>.from(snapshot);
        setState(() {
          // 新增:每次拉取到新数据先清空旧列表,避免重复
          productKeys.clear();
          productNames.clear();
          soldPieces.clear();
          countedPieces.clear();
          productPrices.clear();
          
          countedProductsMap.forEach((key, value) {
            // 新增:存储每个产品的节点key
            productKeys.add(key);
            productNames.add(value['ürünİsmi']);
            soldPieces.add(value['satılanMiktar']);
            countedPieces.add(value['sayılanMiktar']);
            productPrices.add(value['ürünFiyatı']);
          });
        });
    });
  }

  // 新增:更新产品数据的方法
  Future<void> _updateProduct(int index, int newSold, int newCounted, double newPrice) async {
    await firebaseRef
      .child('Sayımlar')
      .child(widget.standName)
      .child('${widget.countDate}')
      .child(productKeys[index])
      .update({
        'satılanMiktar': newSold,
        'sayılanMiktar': newCounted,
        'ürünFiyatı': newPrice,
      });
    if(mounted) {
      ScaffoldMessenger.of(context).showSnackBar(const SnackBar(content: Text('产品更新成功')));
    }
  }

  // 新增:点击卡片弹出编辑窗口的逻辑
  void _showEditDialog(int index) {
    final soldController = TextEditingController(text: soldPieces[index].toString());
    final countedController = TextEditingController(text: countedPieces[index].toString());
    final priceController = TextEditingController(text: productPrices[index].toString());
    showDialog(
      context: context,
      builder: (context) => AlertDialog(
        title: Text('编辑 ${productNames[index]}'),
        content: Column(
          mainAxisSize: MainAxisSize.min,
          children: [
            TextField(
              controller: soldController,
              keyboardType: TextInputType.number,
              decoration: const InputDecoration(labelText: '销售数量'),
            ),
            TextField(
              controller: countedController,
              keyboardType: TextInputType.number,
              decoration: const InputDecoration(labelText: '盘点数量'),
            ),
            TextField(
              controller: priceController,
              keyboardType: TextInputType.numberWithOptions(decimal: true),
              decoration: const InputDecoration(labelText: '产品价格'),
            ),
          ],
        ),
        actions: [
          TextButton(
            onPressed: () => Navigator.pop(context),
            child: const Text('取消'),
          ),
          TextButton(
            onPressed: () {
              final newSold = int.tryParse(soldController.text) ?? soldPieces[index];
              final newCounted = int.tryParse(countedController.text) ?? countedPieces[index];
              final newPrice = double.tryParse(priceController.text) ?? productPrices[index];
              _updateProduct(index, newSold, newCounted, newPrice);
              Navigator.pop(context);
            },
            child: const Text('保存'),
          ),
        ],
      )
    );
  }

  Widget countsCard({
    required int soldPiece,
    required double productPrice,
    required int countedPiece,
    required String productName,
    required int thePieceBeforeCount,
    required double totalPrice,
    // 新增点击回调参数
    required VoidCallback onTap,
  }) {
    return Card(
      color: Colors.white,
      // 新增点击事件
      child: InkWell(
        onTap: onTap,
        child: ListTile(
          isThreeLine: true,
          title: Text(
            productName.toUpperCase(),
            style: kBlackTS.copyWith(fontWeight: FontWeight.bold),
          ),
          subtitle: Column(
            children: [
              Row(
                children: [
                  Text('Sayılan Adet: $countedPiece', style: kBlackTS),
                  emptySpaceWidth(context, 0.04),
                  Text('Satılan Adet: $soldPiece', style: kBlackTS),
                  emptySpaceWidth(context, 0.04),
                  Text('Sayım Öncesi Adet: $thePieceBeforeCount',
                      style: kBlackTS),
                ],
              ),
              emptySpaceHeight(context, 0.015),
              Row(
                children: [
                  Text('Satış Fiyatı: ${productPrice.toStringAsFixed(2)} ₺',
                      style: kBlackTS),
                  emptySpaceWidth(context, 0.04),
                  Text('Toplam Tutar: ${totalPrice.toStringAsFixed(2)} ₺',
                      style: kBlackTS),
                ],
              ),
            ],
          ),
        ),
      ),
    );
  }

  FloatingActionButton get _invoiceFAB {
    return FloatingActionButton(
      backgroundColor: Colors.blue.shade700,
      child: Icon(FontAwesomeIcons.fileInvoice, color: Colors.white),
      // 你可以在这里补充生成发票的逻辑
      onPressed: () => ScaffoldMessenger.of(context).showSnackBar(const SnackBar(content: Text('生成发票功能可在此处扩展')))
    );
  }

  @override
  Widget build(BuildContext context) {
    CustomAppBar customAppBar = CustomAppBar(
      context: context,
      colour: Colors.blue.shade700,
      title: '${widget.countDate} Tarihli Sayımlar');
    return SafeArea(
      child: Scaffold(
        backgroundColor: Colors.blue.shade100,
        appBar: customAppBar.customAppBar,
        body: Container(
          child: Column(
            children: [
              Expanded(
                // 修正:原来的ListView外面包Expanded,避免布局溢出
                child: ListView.builder(
                  shrinkWrap: true,
                  // 修正:空判断逻辑,直接用列表长度即可
                  itemCount: productNames.length,
                  itemBuilder: (context, index) {
                    double total = soldPieces[index] * productPrices[index];
                    int thePieceBeforeCount =
                        soldPieces[index] + countedPieces[index];
                    return countsCard(
                        soldPiece: soldPieces[index],
                        productPrice: productPrices[index],
                        countedPiece: countedPieces[index],
                        productName: productNames[index],
                        thePieceBeforeCount: thePieceBeforeCount,
                        totalPrice: total,
                        // 新增传入点击编辑的逻辑
                        onTap: () => _showEditDialog(index),
                    );
                  },
                ),
              ),
            ],
          ),
        ),
        floatingActionButton: _invoiceFAB,
      ),
    );
  }
}
功能验证方式

修改完成后,点击任意产品卡片即可弹出编辑窗口,修改数值后点击保存,Firebase会自动更新对应节点的数据,onValue监听器会自动拉取最新数据刷新列表,不会再出现重复条目的问题。

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.10.01 23:15:03