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

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.07.03 16:06:04