Flutter GridView中自定义卡片选中状态滚动后丢失问题求助
问题原因分析与解决方案
原因分析
- 子组件
CategoryCard的选中状态isSelected是内部维护的局部状态,当GridView滚动时,超出视口的卡片会被销毁,回到视口时会重新创建,此时局部状态会被重置为初始值true,导致选中状态丢失。 - 错误使用
UniqueKey:给每个卡片加UniqueKey会让GridView每次构建item时都创建全新的CategoryCard实例,完全无法复用,反而加剧了状态丢失的问题,因为每次重新创建的卡片都是全新状态。 - 子组件未与父组件数据源同步:父组件通过
onTap更新了game.categories,但子组件初始化时没有根据这个数据源判断自己是否应该处于选中状态,导致重建后状态和数据源不一致。
解决步骤
1. 子组件改为接收外部选中状态
修改CategoryCard,新增isSelected参数,不再自行维护选中状态:
class CategoryCard extends StatefulWidget { const CategoryCard({ super.key, required this.category, required this.isSelected, // 从父组件传入选中状态 required this.onTap, }); final Category category; final bool isSelected; final VoidCallback onTap; // 简化回调,无需传递isSelected @override State<CategoryCard> createState() => _CategoryCardState(); }
2. 同步动画与外部选中状态
在子组件的生命周期方法中,根据传入的isSelected控制动画控制器,同时用AnimatedBuilder优化动画性能:
class _CategoryCardState extends State<CategoryCard> with TickerProviderStateMixin { late AnimationController controller; late Animation<Color?> containerBackground; late Animation<Color?> imageBackground; late Animation<Color?> textColor; late CurvedAnimation curve; @override void initState() { super.initState(); controller = AnimationController( duration: const Duration(milliseconds: 300), vsync: this, ); curve = CurvedAnimation( parent: controller, curve: Curves.easeInOut, ); // 初始化动画 containerBackground = ColorTween(begin: Colors.white, end: AppColors.blue200) .animate(controller); imageBackground = ColorTween(begin: AppColors.blue50, end: AppColors.blue100) .animate(controller); textColor = ColorTween(begin: AppColors.blue300, end: Colors.white) .animate(controller); // 根据初始选中状态设置动画位置 if (widget.isSelected) { controller.forward(); } else { controller.reverse(); } } @override void didUpdateWidget(covariant CategoryCard oldWidget) { super.didUpdateWidget(oldWidget); // 当外部选中状态变化时,更新动画 if (oldWidget.isSelected != widget.isSelected) { widget.isSelected ? controller.forward() : controller.reverse(); } } @override void dispose() { controller.dispose(); super.dispose(); } @override Widget build(BuildContext context) { return GestureDetector( onTap: widget.onTap, child: AnimatedBuilder( animation: controller, builder: (context, child) { return Container( decoration: BoxDecoration( color: containerBackground.value, borderRadius: Corners.lgBorder, ), child: child, ); }, child: Column(mainAxisAlignment: MainAxisAlignment.center, children: [ Container( padding: EdgeInsets.all(Insets.xs), decoration: BoxDecoration( color: imageBackground.value, borderRadius: Corners.lgBorder, ), clipBehavior: Clip.antiAlias, child: Image.asset( "assets/images/${widget.category.name.toLowerCase()}.png", width: 78, height: 78, ), ), SizedBox(height: Insets.med), Text( widget.category.name, style: TextStyles.body1.copyWith(color: textColor.value), ) ]), ), ); } }
3. 修正父组件GridView逻辑
- 替换
UniqueKey为基于category唯一标识的ValueKey,确保GridView能正确复用widget。 - 从
game.categories判断当前卡片的选中状态,传入子组件。 - 简化
onTap回调,直接更新数据源并刷新UI:
GridView.builder( padding: EdgeInsets.all(Insets.sm), gridDelegate: SliverGridDelegateWithFixedCrossAxisCount( crossAxisCount: 2, mainAxisSpacing: Insets.sm, crossAxisSpacing: Insets.sm, ), itemCount: categories.length, itemBuilder: (BuildContext context, int index) { final category = categories[index]; final isSelected = game.categories.contains(category); return CategoryCard( key: ValueKey(category.id), // 用category的唯一ID作为Key(需确保Category有id字段) category: category, isSelected: isSelected, onTap: () { isSelected ? game.categories.remove(category) : game.categories.add(category); setState(() {}); // 父组件是StatefulWidget时,调用setState刷新UI }, ); }, ),
核心优化点
- 状态提升:将选中状态的控制权交给父组件,子组件仅负责渲染和动画,确保数据源与UI状态一致。
- 正确使用Key:
ValueKey基于category的唯一标识,让GridView能准确复用组件,避免不必要的重建。 - 动画性能优化:用
AnimatedBuilder替代addListener+setState,减少无效重绘。
内容的提问来源于stack exchange,提问作者craig_ck
相关产品推荐
相关产品推荐

