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

Flutter电商APP收藏按钮异常:点击选中全部商品求助

Flutter电商APP收藏按钮异常:点击单个商品收藏导致所有商品被标记为收藏

问题描述

开发Flutter电商APP时,采用TabBarView嵌套GridView展示商品,点击某商品的「添加到收藏」按钮后,所有商品都会被标记为已收藏,而非仅操作的单个商品。

状态截图

选中前的商品状态

选中前的商品状态

选中后的状态(所有商品被标记为收藏)

选中后的商品状态

相关代码

// Bottle and Bottle Category Model classes
class Bottle with ChangeNotifier {
  String id;
  String bottleName;
  String image;
  String bottletype;
  int time;
  double price;
  bool isFavourite;
  Bottle(
      {required this.id,
      required this.bottleName,
      required this.image,
      required this.bottletype,
      required this.time,
      required this.price,
      this.isFavourite = false});

  void isFavoritePressed() {
    isFavourite = !isFavourite;
    notifyListeners();
  }
}

class BottleCategory {
  int categoryNo;
  String bottleType;
  List<Bottle> bottleList;
  BottleCategory(
      {required this.categoryNo,
      required this.bottleType,
      required this.bottleList});
}
// Cart Model Class
class Cart with ChangeNotifier {
  String id;
  String bottleName;
  String image;
  double price;
  int quantity;
  Cart({
    required this.id,
    required this.bottleName,
    required this.image,
    required this.price,
    required this.quantity,
  });
}


class ProductItem extends StatelessWidget {
  HomePage homeInstance = const HomePage();
  String id;
  String bottleName;
  String imgUrl;
  double price;
  bool isFavourite;
  ProductItem(
      {Key? key,
      required this.id,
      required this.bottleName,
      required this.imgUrl,
      required this.price,
      this.isFavourite = false})
      : super(key: key);

  @override
  Widget build(BuildContext context) {
    var cart = Provider.of<ShoppingCartProvider>(context);
    var product = Provider.of<Bottle>(context);
    return Card(
      shadowColor: Colors.grey,
      surfaceTintColor: Colors.amber,
      shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(20)),
      child: Stack(
        children: [
          Positioned(
            right: 0,
            child: InkWell(
              onTap: () {
                product.isFavoritePressed();
                if (product.isFavourite) {
                  ScaffoldMessenger.of(context).hideCurrentSnackBar();
                  ScaffoldMessenger.of(context).showSnackBar(
                    const SnackBar(
                      content: Text(
                        "Product Added to Favorite!",
                        style: TextStyle(fontSize: 16),
                      ),
                      backgroundColor: Colors.green,
                      duration: Duration(seconds: 1),
                    ),
                  );
                } else {
                  ScaffoldMessenger.of(context).hideCurrentSnackBar();
                  ScaffoldMessenger.of(context).showSnackBar(
                    const SnackBar(
                      content: Text(
                        "Product Removed from Favorite!",
                        style: TextStyle(fontSize: 16),
                      ),
                      backgroundColor: Colors.red,
                      duration: Duration(seconds: 1),
                    ),
                  );
                }
              },
              child: Icon(
                product.isFavourite ? Icons.favorite : Icons.favorite_border,
                size: 25,
                color: Colors.redAccent,
              ),
            ),
          ),
          Column(
            mainAxisAlignment: MainAxisAlignment.center,
            children: [
              Center(
                child: Image.asset(
                  imgUrl,
                  height: 200.0,
                ),
              ),
              Center(
                  child: Text(bottleName,
                      style: const TextStyle(
                          fontSize: 20.0, fontWeight: FontWeight.bold))),
              Center(
                child: Text('R$price'),
              )
            ],
          ),
          Positioned(
              bottom: 0,
              right: 10,
              child: IconButton(
                icon: const Icon(Icons.add_circle),
                iconSize: 40.0,
                onPressed: () {
                  cart.addToCart(id, bottleName, price, imgUrl);
                },
              ))
        ],
      ),
    );
  }
}

class HomePage extends StatefulWidget {
  const HomePage({Key? key}) : super(key: key);

  @override
  State<HomePage> createState() => _HomePageState();
}

class _HomePageState extends State<HomePage> with TickerProviderStateMixin {
  ProductProvider productProvider = ProductProvider();
  late TabController tabController;
  late Bottle bottleProvider;

  String query = " ";

  Widget buildSearch() {
    return SearchWidget(
      text: query,
      hintText: 'Search for beverages',
      onChanged: (value) {
        setState(() {
          this.query = value;
        });
      },
    );
  }

  @override
  void initState() {
    super.initState();
    tabController = TabController(length: 4, vsync: this);
  }

  @override
  void dispose() {
    tabController.dispose();
    super.dispose();
  }

  @override
  Widget build(BuildContext context) {
    var cart = Provider.of<ShoppingCartProvider>(context);
    Size _screenSize = MediaQuery.of(context).size;
    final double itemHeight = (_screenSize.height - kToolbarHeight - 24) / 2;
    final double itemWidth = _screenSize.width / 2;
    return Scaffold(
      body: SingleChildScrollView(
        child: Column(
          mainAxisAlignment: MainAxisAlignment.start,
          crossAxisAlignment: CrossAxisAlignment.start,
          children: [
            buildSearch(),
            const Padding(
              padding: EdgeInsets.all(8.0),
              child: Text(
                'Categories',
                style: TextStyle(
                    fontSize: 20.0,
                    fontFamily: 'Montserrat-ExtraBold',
                    fontWeight: FontWeight.bold),
              ),
            ),
            Container(
              child: Align(
                alignment: Alignment.centerLeft,
                child: TabBar(
                  controller: tabController,
                  indicator:
                      CircleTabIndicator(color: Colors.redAccent, radius: 4.0),
                  isScrollable: true,
                  labelColor: Colors.redAccent,
                  labelStyle: const TextStyle(
                      fontWeight: FontWeight.bold, fontSize: 20.0),
                  unselectedLabelColor: Colors.black,
                  unselectedLabelStyle: const TextStyle(
                      fontWeight: FontWeight.bold, fontSize: 20.0),
                  tabs: const [
                    Tab(text: 'Brandy'),
                    Tab(text: 'Gin'),
                    Tab(text: 'Soft drinks'),
                    Tab(text: 'Whiskey')
                  ],
                ),
              ),
            ),
            Container(
              height: 400,
              width: double.maxFinite,
              child: TabBarView(
                  controller: tabController,
                  children: productProvider.categories.map((bottleCategory) {
                    return GridView.count(
                      crossAxisCount: 2,
                      childAspectRatio: itemWidth / itemHeight,
                      children: bottleCategory.bottleList.where((element) {
                        return element.bottleName.toLowerCase().contains(
                            query.toLowerCase()); // Apply the query string
                      }).map((bottle) {
                        return ProductItem(
                          id: bottle.id,
                          bottleName: bottle.bottleName,
                          imgUrl: bottle.image,
                          price: bottle.price,
                        );
                      }).toList(),
                    );
                  }).toList()),
            ),
          ],
        ),
      ),
    );
  }
}
// ProductProvider
class ProductProvider with ChangeNotifier {
  final List<BottleCategory> categories = [
    BottleCategory(categoryNo: 1, bottleType: 'Brandy', bottleList: [
      Bottle(
          id: 'KLPB',
          bottleName: 'Klipdrift Brandy',
          image: 'lib/assets/images/KlipdriftBrandy.png',
          time: 20,
          price: 274.00,
          bottletype: 'Brandy'),
      Bottle(
          id: 'KWV3',
          bottleName: 'Kwv Brandy 3 years',
          image: 'lib/assets/images/KwvBrandy3years.jpg',
          time: 20,
          price: 274.00,
          bottletype: 'Brandy'),
    ]),
    BottleCategory(categoryNo: 2, bottleType: 'Gin', bottleList: [
      Bottle(
          id: 'BMBS',
          bottleName: 'Bombay Sapphire Gin',
          image: 'lib/assets/images/BombaySapphireGin.png',
          time: 20,
          price: 274.00,
          bottletype: 'Gin'),
      Bottle(
          id: 'TNGN',
          bottleName: 'Tanqueray Gin',
          image: 'lib/assets/images/TanquerayGin.png',
          time: 20,
          price: 274.00,
          bottletype: 'Gin'),
    ]),
    BottleCategory(categoryNo: 3, bottleType: 'Soft Drinks', bottleList: [
      Bottle(
          id: 'COCA',
          bottleName: 'Coca Cola',
          image: 'lib/assets/images/CocaCola.jpg',
          time: 20,
          price: 274.00,
          bottletype: 'Soft Drinks'),
      Bottle(
          id: 'SPRT',
          bottleName: 'Sprite',
          image: 'lib/assets/images/Sprite.jpg',
          time: 20,
          price: 274.00,
          bottletype: 'Soft Drinks'),
    ]),
    BottleCategory(categoryNo: 4, bottleType: 'Whiskey', bottleList: [
      Bottle(
          id: '',
          bottleName: 'Jameson',
          image: 'lib/assets/images/Jameson.jpg',
          time: 20,
          price: 274.00,
          bottletype: 'Whiskey'),
      Bottle(
          id: '',
          bottleName: 'Johnnie Walker Black Label',
          image: 'lib/assets/images/JohnnieWalkerBlackLabel.jpg',
          time: 20,
          price: 274.00,
          bottletype: 'Whiskey'),
    ]),
  ];
  List<Bottle> get bottleItems {
    return [...bottleItems];
  }

  List<Bottle> get bottleObjects {
    return [...bottleObjects];
  }

  get bottle => null;

  List<Bottle> get selectedFavorite {
    return categories
        .expand((bottleCategory) {
          return bottleCategory.bottleList;
        })
        .toList()
        .where((bottle) {
          return bottle.isFavourite;
        })
        .toList();
  }
}

// Cart Provider
import 'package:flutter/cupertino.dart';
import 'package:test_code/cart.dart';

class ShoppingCartProvider with ChangeNotifier {
  final Map<String, Cart> _cartItems = {};

  Map<String, Cart> get cartItem {
    return {..._cartItems};
  }

  void removeBottle(String id) {
    _cartItems.remove(id);
    notifyListeners();
  }

  void addToCart(
      String id, String bottleName, double bottlePrice, String image) {
    if (_cartItems.containsKey(id)) {
      _cartItems.update(id, (existingItem) {
        return Cart(
            id: existingItem.id,
            bottleName: existingItem.bottleName,
            image: existingItem.image,
            price: existingItem.price,
            quantity: existingItem.quantity + 1);
      });
    } else {
      _cartItems.putIfAbsent(id, () {
        return Cart(
            id: id,
            bottleName: bottleName,
            image: image,
            price: bottlePrice,
            quantity: 1);
      });
    }
    notifyListeners();
  }

  void addQuantity(String id) {
    _cartItems.update(id, (existingItem) {
      return Cart(
          id: existingItem.id,
          bottleName: existingItem.bottleName,
          image: existingItem.image,
          price: existingItem.price,
          quantity: existingItem.quantity + 1);
    });
    notifyListeners();
  }

  void reduceQuantity(String id) {
    _cartItems.update(id, (existingItem) {
      if (existingItem.quantity == 1) {
        return Cart(
            id: existingItem.id,
            bottleName: existingItem.bottleName,
            image: existingItem.image,
            price: existingItem.price,
            quantity: existingItem.quantity);
      } else {
        return Cart(
            id: existingItem.id,
            bottleName: existingItem.bottleName,
            image: existingItem.image,
            price: existingItem.price,
            quantity: existingItem.quantity - 1);
      }
    });
    notifyListeners();
  }

  void clearCart() {
    _cartItems.clear();
    notifyListeners();
  }

  double get totalAmount {
    double total = 0.0;
    _cartItems.forEach((key, eachItem) {
      total = total + eachItem.price * eachItem.quantity;
    });
    return total;
  }

  int get cartCount {
    return _cartItems.length;
  }
}

问题原因

  1. 状态管理错误:Bottle类继承了ChangeNotifier,但未为每个商品实例在Widget树中提供独立的Provider。ProductItem中直接调用Provider.of<Bottle>(context)会导致所有组件获取到同一个(或错误的)Bottle实例,触发状态变化时所有商品同步更新。
  2. 参数未正确使用:ProductItem接收了isFavourite参数但未使用,而是依赖Provider获取状态,进一步加剧了状态共享问题。
  3. 无限递归错误:ProductProvider中的bottleItems getter返回[...bottleItems],会导致无限递归调用。
  4. 商品ID缺失:Whiskey分类下的两个商品无唯一ID,导致无法精准定位切换收藏状态的商品。

解决方案

1. 重构Bottle类,移除ChangeNotifier继承

单个商品状态应由上层ProductProvider统一管理,而非每个商品独立作为状态提供者:

class Bottle {
  String id;
  String bottleName;
  String image;
  String bottletype;
  int time;
  double price;
  bool isFavourite;
  
  Bottle({
    required this.id,
    required this.bottleName,
    required this.image,
    required this.bottletype,
    required this.time,
    required this.price,
    this.isFavourite = false,
  });

  // 返回新实例切换收藏状态(保持immutable特性)
  Bottle toggleFavorite() {
    return Bottle(
      id: id,
      bottleName: bottleName,
      image: image,
      bottletype: bottletype,
      time: time,
      price: price,
      isFavourite: !isFavourite,
    );
  }
}

2. 完善ProductProvider的状态管理逻辑

添加精准切换收藏状态的方法,修复无限递归问题:

class ProductProvider with ChangeNotifier {
  final List<BottleCategory> categories = [
    // 保留原分类数据,补充Whiskey商品的ID
    BottleCategory(categoryNo: 4, bottleType: 'Whiskey', bottleList: [
      Bottle(
          id: 'JMSN',
          bottleName: 'Jameson',
          image: 'lib/assets/images/Jameson.jpg',
          time: 20,
          price: 274.00,
          bottletype: 'Whiskey'),
      Bottle(
          id: 'JWBL',
          bottleName: 'Johnnie Walker Black Label',
          image: 'lib/assets/images/JohnnieWalkerBlackLabel.jpg',
          time: 20,
          price: 274.00,
          bottletype: 'Whiskey'),
    ]),
  ];

  // 获取所有商品列表
  List<Bottle> get bottleItems {
    return categories.expand((category) => category.bottleList).toList();
  }

  // 切换指定ID商品的收藏状态
  void toggleFavorite(String bottleId) {
    for (var category in categories) {
      final index = category.bottleList.indexWhere((b) => b.id == bottleId);
      if (index != -1) {
        category.bottleList[index] = category.bottleList[index].toggleFavorite();
        notifyListeners();
相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.08.20 11:15:41