Flutter单选Checkbox实现:选中时自动取消其他选项
解决方案
核心问题分析
当前代码的问题在于:
- 每次
build方法执行时,_colorSelected都会在FutureBuilder中被重新初始化为全false,导致状态丢失 - 选中单个Checkbox时,仅修改当前索引的状态,未将其他Checkbox设为未选中
- 使用
StatefulBuilder局部管理状态,导致页面级状态无法同步 isColorSelected定义在build方法内部,每次构建都会重置为false
修改步骤及代码实现
1. 调整类成员变量
将isColorSelected提升为类成员变量,避免每次构建重置:
class _ProductDetailsState extends ConsumerState<ProductDetails> with TickerProviderStateMixin{ late TabController _controller; late List<bool> _colorSelected; bool isColorSelected = false; // 移到类成员 // ... 其他代码 }
2. 优化_colorSelected初始化逻辑
避免在FutureBuilder中重复初始化数组,仅在数据首次加载或颜色数量变化时更新:
if(snapshot.data != null){ final data = snapshot.data as Map<String,dynamic>; final colors =data['colors']; // 新增标记判断,避免重复初始化 bool _isColorsLoaded = false; // 仅当数组未初始化或长度变化时重新赋值 if (!_isColorsLoaded || _colorSelected.length != colors.length) { _colorSelected = List<bool>.filled(colors.length, false, growable: false); _isColorsLoaded = true; } // ... 其他代码 }
3. 实现单选逻辑
修改Checkbox的onChanged回调,选中当前项时将其他所有项设为未选中:
onChanged: (val) { setState((){ // 先将所有选项设为未选中 for(int i=0; i<_colorSelected.length; i++){ _colorSelected[i] = false; } // 再设置当前选项为选中状态 _colorSelected[index] = val!; isColorSelected = true; }); checkPrice(colors[index]); },
4. 移除不必要的StatefulBuilder
使用页面的setState管理状态即可,局部的StatefulBuilder不再需要:
// 移除StatefulBuilder包裹,直接返回Transform.scale return Transform.scale( scale: 1.5.h, child: Checkbox( // ... 原有Checkbox属性 value: _colorSelected[index], onChanged: (val) { setState((){ for(int i=0; i<_colorSelected.length; i++){ _colorSelected[i] = false; } _colorSelected[index] = val!; isColorSelected = true; }); checkPrice(colors[index]); }, ), );
完整修改后的关键代码片段
class _ProductDetailsState extends ConsumerState<ProductDetails> with TickerProviderStateMixin{ late TabController _controller; late List<bool> _colorSelected; bool isColorSelected = false; bool _isColorsLoaded = false; // 新增标记,避免重复初始化 @override void initState() { _controller = TabController(length: 2, vsync: this); _colorSelected = []; // 初始化空数组 super.initState(); } @override Widget build(BuildContext context) { return Container( color: Colors.grey.shade300, child: SafeArea( child: Scaffold( backgroundColor: Colors.white, body: Container( margin: EdgeInsets.only( top: 30.h, left: 20.w, right: 20.w, bottom: 30.h, ), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ // ... 原有顶部导航代码 SizedBox(height: 20.h,), FutureBuilder( future: FetchProductDetails().getDetails(widget.inventoryId), builder: (context,snapshot){ if(snapshot.data != null){ final data = snapshot.data as Map<String,dynamic>; final colors =data['colors']; // 仅在首次加载或颜色数量变化时初始化数组 if (!_isColorsLoaded || _colorSelected.length != colors.length) { _colorSelected = List<bool>.filled(colors.length, false, growable: false); _isColorsLoaded = true; } void checkPrice(Map colorCode){ List price = data['prices']; var finalPrice = price.where((element) => element['colorId'] == colorCode['inventoryColorId']).toList(); print(finalPrice); } return Expanded( child: SingleChildScrollView( child: Column( children: [ // ... 原有商品图片、名称、店铺信息代码 SizedBox(height: 12.h,), Row( mainAxisAlignment: MainAxisAlignment.start, children: [ SizedBox( height: 28.h, child: ListView.builder( scrollDirection: Axis.horizontal, shrinkWrap: true, itemCount: colors.length, itemBuilder: (context,index){ final hex = colors[index]['code']; String color = hex.replaceAll('#', '0xff'); return Row( mainAxisAlignment: MainAxisAlignment.start, children: [ Transform.scale( scale: 1.5.h, child: Checkbox( materialTapTargetSize: MaterialTapTargetSize.shrinkWrap, side: MaterialStateBorderSide.resolveWith( (states) => const BorderSide(width: 1.0, color: Colors.transparent), ), fillColor:MaterialStateProperty.resolveWith((states) { if (states.contains(MaterialState.pressed)) { return Color(int.parse(color)); } return Color(int.parse(color)); }), overlayColor: MaterialStateProperty.resolveWith((states) { if (states.contains(MaterialState.pressed)) { return Color(int.parse(color)); } return Color(int.parse(color)); }), activeColor: AppColors.primaryColor6, value: _colorSelected[index], onChanged: (val) { setState((){ // 单选逻辑:取消所有选中,再选中当前项 for(int i=0; i<_colorSelected.length; i++){ _colorSelected[i] = false; } _colorSelected[index] = val!; isColorSelected = true; }); checkPrice(colors[index]); }, ), ), ], ); }), ), ], ), ], ), ), ); }else{ print(snapshot.error); return const Center(child: CircularProgressIndicator(),); } }) ], ), ), ), ), ); } }
额外优化建议
- 考虑使用
Radio组件替代Checkbox:单选场景更符合Radio的设计语义,用户体验更直观 - 价格显示逻辑可以优化:目前两个Text组件内容完全一致,可根据选中状态动态更新价格,避免重复代码
内容的提问来源于stack exchange,提问作者Homerunner
相关产品推荐
相关产品推荐

