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; } }
问题原因
- 状态管理错误:
Bottle类继承了ChangeNotifier,但未为每个商品实例在Widget树中提供独立的Provider。ProductItem中直接调用Provider.of<Bottle>(context)会导致所有组件获取到同一个(或错误的)Bottle实例,触发状态变化时所有商品同步更新。 - 参数未正确使用:
ProductItem接收了isFavourite参数但未使用,而是依赖Provider获取状态,进一步加剧了状态共享问题。 - 无限递归错误:
ProductProvider中的bottleItemsgetter返回[...bottleItems],会导致无限递归调用。 - 商品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();
相关产品推荐
相关产品推荐

