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

Flutter+Firebase+GetX购物车:如何避免重复条目并增加商品数量?

购物车重复条目问题排查与修复

我用GetX和Firebase开发购物车功能,期望用户点击「加入购物车」时,不生成重复条目,只增加对应商品数量,但现在出现了重复条目。

CartController代码

class CartController extends GetxController {
  static CartController instance = Get.find();
  //cart controller for the shopping cart
  Rx<double> totalCartPrice = 0.0.obs;
  RxList<CartItemModel> products = RxList<CartItemModel>([]);
  @override
  void onReady() {
    super.onReady();
    ever(logController.userModel, editTotalPrice);
  }

  void addToCart(ProductModel product) {
    try {
      if (_isItemAlreadyAdded(product)) {
        Get.snackbar("Check your cart", "${product.name} is already added");
      } else {
        String itemID = Uuid().v1();
        logController.updateUserData({
          "cart": FieldValue.arrayUnion([
            {
              "id": itemID,
              "productId": product.id,
              // "name": product.name,
              "quantity": 1,
              "price": product.price,
              "image": product.image,
              "cost": product.price,
            }
          ])
        });
        Get.snackbar("Item added", "${product.name} was added to your cart");
      }
    } catch (e) {
      Get.snackbar("Whoops...Cannot add this right now!",
          "There was an error adding to cart",
          duration: Duration(seconds: 3), backgroundColor: Colors.pinkAccent);
      debugPrint(e.toString());
    }
  }

  void deductCartItem(CartItemModel cartItem) {
    try {
      logController.updateUserData({
        "cart": FieldValue.arrayRemove([cartItem.toJson()])
      });
    } catch (e) {
      Get.snackbar("Error", "Cannot remove this item");
      debugPrint(e.toString());
    }
  }

  editTotalPrice(LoginUserModel usrModel) {
    if (usrModel.cart!.isEmpty) {
      print("Cart empty!");
    } else if (usrModel.cart!.isNotEmpty) {
      totalCartPrice.value = 50;
      print("hi");
      usrModel.cart!.forEach((cartItem) {
        totalCartPrice.value += cartItem.cost!;
      });
    }
  }

  bool _isItemAlreadyAdded(ProductModel product) =>
      logController.userModel.value.cart!
          .where((item) => item.name == product.name)
          .isNotEmpty;

  void decreaseQuantity(CartItemModel item) {
    if (item.quantity == 1) {
      deductCartItem(item);
    } else {
      deductCartItem(item);
      item.quantity! - 1;
      logController.updateUserData({
        "cart": FieldValue.arrayUnion([item.toJson()])
      });
    }
  }

  void increaseQuantity(CartItemModel item) {
    deductCartItem(item);
    item.quantity! + 1;
    logger.i({"quantity": item.quantity});
    logController.updateUserData({
      "cart": FieldValue.arrayUnion([item.toJson()])
    });
  }
}

CartItemModel模型类

class CartItemModel {
  CartItemModel(
      {this.productId,
      this.id,
      this.image,
      this.name,
      this.quantity,
      required this.price,
      this.cost});

  CartItemModel.fromMap(Map<String, dynamic> data) {
    id = data[ID];
    image = data[IMAGE];
    name = data[NAME];
    quantity = data[QUANTITY];
    cost = data[COST].toDouble();
    productId = data[PRODUCT_ID];
    price = data[PRICE];
  }

  static const COST = "cost";
  static const ID = "id";
  static const IMAGE = "image";
  static const NAME = "name";
  static const PRICE = "price";
  static const PRODUCT_ID = "productId";
  static const QUANTITY = "quantity";

  double? cost;
  String? id;
  String? image;
  String? name;
  double? price;
  String? productId;
  int? quantity;

  Map toJson() => {
        ID: id,
        PRODUCT_ID: productId,
        IMAGE: image,
        NAME: name,
        QUANTITY: quantity,
        COST: price! * quantity!,
        PRICE: price
      };
}

LoginUserModel登录相关模型类

class LoginUserModel {
  String? displayName;
  String? email;
  String? photoUrl;
  String? uid;
  List<CartItemModel>? cart;

  LoginUserModel(
      {this.displayName, this.email, this.photoUrl, this.uid, this.cart});

  LoginUserModel.fromSnapshot(DocumentSnapshot<Map<String, dynamic>> snapshot) {
    displayName = snapshot.data()!["DISPLAYNAME"];
    photoUrl = snapshot.data()!["PHOTOURL"];
    email = snapshot.data()!["EMAIL"];
    uid = snapshot.data()!["UID"];
    cart = _convertCartItems(snapshot.data()!["CART"] ?? []);
  }

  List<CartItemModel> _convertCartItems(List cartFomDb) {
    List<CartItemModel> _result = [];
    if (cartFomDb.length > 0) {
      cartFomDb.forEach((element) {
        _result.add(CartItemModel.fromMap(element));
      });
    }
    return _result;
  }

  List cartItemsToJson() => cart!.map((item) => item.toJson()).toList();
}

LoginController登录控制器代码

class LoginController extends GetxController {
  static LoginController instance = Get.find();

  Rxn<User> fbUser = Rxn<User>();
  final googleSignIn = GoogleSignIn();
  RxBool isLoggedIn = false.obs;
  Rx<LoginUserModel> userModel = LoginUserModel().obs;
  String usersCollection = "users";

  GoogleSignInAccount? _googleAcc;
  LoginUserModel? _userModel;

  @override
  void onReady() {
    super.onReady();
    fbUser = Rxn<User>(auth.currentUser);
    fbUser.bindStream(auth.userChanges());
    ever(fbUser, setInitialScreen);
  }

  LoginUserModel? get loggedInUserModel => _userModel;

  setInitialScreen(User? user) {
    if (user == null) {
      print("going to login page...");
      Get.offAll(() => LoginPage());
    } else {
      print("The user is ${user.displayName}");
      userModel.bindStream(listenToUser());
      Get.offAll(() => AppSetup());
    }
  }

  void googleLogin() async {
    final googleUser = await googleSignIn.signIn();
    if (googleUser == null) return;

    _googleAcc = googleUser;
    final googleAuth = await googleUser.authentication;

    final cred = GoogleAuthProvider.credential(
      accessToken: googleAuth.accessToken,
      idToken: googleAuth.idToken,
    );
    try {
      await auth.signInWithCredential(cred).then((res) async {
        print('Signed in successfully as ' + res.user!.displayName.toString());
        print('email: ' + res.user!.email.toString());
        LoginUserModel _newUser = LoginUserModel(
          uid: res.user!.uid,
          email: res.user!.email!,
          displayName: res.user!.displayName,
          photoUrl: res.user!.photoURL,
          cart: [],
        );
        _addUserToFB(_newUser, res.user!);
      });
    } catch (e) {
      debugPrint(e.toString());
      Get.snackbar("Sign In Failed", "Try again");
    }
  }

  void signOut() async {
    googleSignIn.signOut();
    auth.signOut();
  }

  updateUserData(Map<String, dynamic> data) {
    logger.i("UPDATED");
    firebaseFirestore
        .collection(usersCollection)
        .doc(fbUser.value?.uid)
        .update(data);
  }

  Stream<LoginUserModel> listenToUser() => firebaseFirestore
      .collection(usersCollection)
      .doc(fbUser.value!.uid)
      .snapshots()
      .map((snapshot) => LoginUserModel.fromSnapshot(snapshot));

  _addUserToFB(LoginUserModel usr, User firebaseUser) {
    firebaseFirestore.collection(usersCollection).doc(usr.uid).set({
      "displayName": usr.displayName,
      "id": usr.uid,
      "photoURL": usr.photoUrl,
      "email": usr.email,
      "cart": usr.cart
    });
  }
}

Firebase数据库结构截图

Firebase用户购物车数据结构


问题根源与修复方案

1. 重复判断逻辑失效

  • 原代码用商品名称判断重复,但addToCart中注释了name字段的写入,导致Firebase中无name值,判断永远不成立,每次都会新增条目。
  • 修复:改用唯一标识productId判断重复,并取消name字段注释:
bool _isItemAlreadyAdded(ProductModel product) =>
    logController.userModel.value.cart!
        .where((item) => item.productId == product.id)
        .isNotEmpty;

// addToCart中取消注释
"name": product.name,

2. 数量增减逻辑无效果

  • 原代码中item.quantity! + 1仅做计算未赋值,导致数量未更新。
  • 修复:给item.quantity赋值:
void decreaseQuantity(CartItemModel item) {
  if (item.quantity == 1) {
    deductCartItem(item);
  } else {
    deductCartItem(item);
    item.quantity = item.quantity! - 1;
    logController.updateUserData({
      "cart": FieldValue.arrayUnion([item.toJson()])
    });
  }
}

void increaseQuantity(CartItemModel item) {
  deductCartItem(item);
  item.quantity = item.quantity! + 1;
  logger.i({"quantity": item.quantity});
  logController.updateUserData({
    "cart": FieldValue.arrayUnion([item.toJson()])
  });
}

3. 优化加入购物车逻辑:已存在则直接加数量

原逻辑仅提示已添加,未实现数量递增,修改addToCart:

void addToCart(ProductModel product) {
  try {
    final existingItem = logController.userModel.value.cart!
        .firstWhereOrNull((item) => item.productId == product.id);
    
    if (existingItem != null) {
      deductCartItem(existingItem);
      existingItem.quantity = existingItem.quantity! + 1;
      logController.updateUserData({
        "cart": FieldValue.arrayUnion([existingItem.toJson()])
      });
      Get.snackbar("数量更新", "${product.name} 数量已增加");
    } else {
      String itemID = Uuid().v1();
      logController.updateUserData({
        "cart": FieldValue.arrayUnion([
          {
            "id": itemID,
            "productId": product.id,
            "name": product.name,
            "quantity": 1,
            "price": product.price,
            "image": product.image,
            "cost": product.price,
          }
        ])
      });
      Get.snackbar("商品已添加", "${product.name} 已加入购物车");
    }
  } catch (e) {
    Get.snackbar("添加失败", "当前无法添加该商品",
        duration: Duration(seconds: 3), backgroundColor: Colors.pinkAccent);
    debugPrint(e.toString());
  }
}

注:需在pubspec.yaml添加collection: ^1.17.2以使用firstWhereOrNull。

4. 修正Firestore字段名大小写不匹配问题

LoginUserModel.fromSnapshot中使用大写字段名,但LoginController._addUserToFB中用小写,导致数据读取失败:

// LoginUserModel.fromSnapshot修改为:
displayName = snapshot.data()!["displayName"];
photoUrl = snapshot.data()!["photoURL"];
email = snapshot.data()!["email"];
uid = snapshot.data()!["id"];
cart = _convertCartItems(snapshot.data()!["cart"] ?? []);

5. 修复总价计算初始值错误

原代码初始值设为50,改为从0开始:

void editTotalPrice(LoginUserModel usrModel) {
  if (usrModel.cart!.isEmpty) {
    totalCartPrice.value = 0.0;
    print("购物车为空!");
  } else {
    totalCartPrice.value = 0.0;
    usrModel.cart!.forEach((cartItem) {
      totalCartPrice.value += cartItem.cost!;
    });
  }
}

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.08.19 00:01:39