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

Flutter中DropdownButton2切换选项时报值匹配错误的求助

联动DropdownButton2下拉框切换地区时触发重复值错误的解决方案

我在Flutter项目中使用dropdown_button2包实现了两个联动下拉框:第一个下拉框从API加载地区选项,第二个下拉框根据第一个的选中项加载对应品牌选项。首次选择地区后品牌下拉框正常工作,但第二次切换地区时,尽管API成功获取了品牌数据,却立刻抛出错误:

"There should be exactly one item with [DropdownButton]'s value: '$value. \n'Either zero or 2 or more [DropdownMenuItem]s were detected 'with the same value'"


联动下拉框使用代码

Row(
  children: [
    CustomDropdownWidget<String>(
      key: Key('location_dropdown'),
      dropdownOptions: _locationStringList,
      onItemSelected: (String selectedValue) {
        print('Selected Value: $selectedValue');
        _getBrandsForLocation(selectedValue.toString());
      },
      label: 'Location',
    ),
    SizedBox(width: 8),
    AbsorbPointer(
      absorbing: !_isLocationSelected,
      child: CustomDropdownWidget<String>(
        key: Key('brand_dropdown'),
        dropdownOptions: _brandStringList,
        onItemSelected: (String selectedValue) {
          print('Selected Value: $selectedValue');
          _setSelectedBrand(selectedValue.toString());
        },
        label: 'Brand',
      ),
    ),
  ],
),

通用下拉框组件代码

class CustomDropdownWidget<String> extends StatefulWidget {
  final List<String> dropdownOptions;
  final ValueChanged<String> onItemSelected;
  final String label;

  const CustomDropdownWidget({
    Key? key,
    required this.dropdownOptions,
    required this.onItemSelected,
    required this.label,
  }) : super(key: key);

  @override
  _CustomDropdownWidgetState<String> createState() =>
      _CustomDropdownWidgetState<String>();
}

class _CustomDropdownWidgetState<String>
    extends State<CustomDropdownWidget<String>> {
  String? _selectedOption;
  String? _label;

  @override
  void initState() {
    _label = widget.label;
    super.initState();
  }

  List<DropdownMenuItem<String>> _addDividersAfterItems(List<String> items) {
    final List<DropdownMenuItem<String>> menuItems = [];
    for (final item in items) {
      menuItems.addAll(
        [
          DropdownMenuItem<String>(
            value: item,
            child: Padding(
              padding: const EdgeInsets.symmetric(horizontal: 8.0),
              child: Text(
                item.toString(),
                style: tBodyMedium6,
              ),
            ),
          ),
          //If it's last item, we will not add Divider after it.
          if (item != items.last)
            const DropdownMenuItem(
              enabled: false,
              child: Divider(
                color: cGreyScale1,
              ),
            ),
        ],
      );
    }
    return menuItems;
  }

  List<double> _getCustomItemsHeights() {
    final List<double> itemsHeights = [];
    for (int i = 0; i < (widget.dropdownOptions.length * 2) - 1; i++) {
      if (i.isEven) {
        itemsHeights.add(40);
      }
      //Dividers indexes will be the odd indexes
      if (i.isOdd) {
        itemsHeights.add(4);
      }
    }
    return itemsHeights;
  }

  @override
  Widget build(BuildContext context) {
    return Container(
      color: cWhite,
      width: 220,
      height: 44,
      child: Stack(
        children: [
          Align(
            alignment: Alignment.centerRight,
            child: Container(
              width: 44,
              height: 44,
              decoration: BoxDecoration(
                borderRadius: BorderRadius.only(
                  topRight: Radius.circular(5),
                  bottomRight: Radius.circular(5),
                ),
                border: Border.all(width: 1, color: cGreyScale1),
                color: cGreyScale0,
              ),
            ),
          ),
          DropdownButtonHideUnderline(
            child: DropdownButton2<String>(
              isExpanded: true,
              items:
                  _addDividersAfterItems(widget.dropdownOptions.cast<String>()),
              value: _selectedOption,
              hint: Text(
                '${_label}',
                style: tBodyMedium4,
              ),
              onChanged: (String? newValue) {
                if (newValue != null) {
                  setState(() {
                    _selectedOption = newValue;
                  });
                  widget.onItemSelected(newValue);
                }
              },
              buttonStyleData: ButtonStyleData(
                width: 220,
                height: 44,
                padding: const EdgeInsets.only(left: 12, right: 10),
                decoration: BoxDecoration(
                  color: Colors.transparent,
                  borderRadius: BorderRadius.circular(5),
                  border: Border.all(width: 1, color: cGreyScale1),
                ),
              ),
              iconStyleData: const IconStyleData(
                icon: Icon(IconlyLight.arrow_down_2),
                iconSize: 24,
                iconEnabledColor: cGreyScale3,
                iconDisabledColor: cGreyScale3,
              ),
              dropdownStyleData: DropdownStyleData(
                width: 220,
                elevation: 4,
                decoration: BoxDecoration(
                  borderRadius: BorderRadius.circular(5),
                  color: cWhite,
                ),
                offset: const Offset(0, -5),
                padding: EdgeInsets.zero,
              ),
              menuItemStyleData: MenuItemStyleData(
                padding: EdgeInsets.zero,
                customHeights: _getCustomItemsHeights(),
              ),
            ),
          ),
        ],
      ),
    );
  }
}

数据获取代码

void _getLocations() async {
  BotToast.showLoading(
    backButtonBehavior: BackButtonBehavior.none,
  );
  _locationList = await _repository!.getLocationsByPartnerID();
  List<String> itemList = [];
  _locationList.forEach((e) {
    itemList.add(e.name!);
  });
  setState(() {
    _locationStringList = itemList;
  });
  BotToast.closeAllLoading();
}

void _getBrandsForLocation(String location) async {
  BotToast.showLoading(
    backButtonBehavior: BackButtonBehavior.none,
  );
  _setSelectedLocation(location);

  if (_brandStringList.isNotEmpty) {
    setState(() {
      _brandStringList = [];
      _selectedBrand = null;
      _isLocationSelected = false;
    });
  }
  _brandList = await _repository!.getBrandsByID(_selectedLocation!);
  List<String> itemList = [];
  _brandList.forEach((e) {
    itemList.add(e.name!.text!);
  });

  setState(() {
    _brandStringList = itemList;
    _isLocationSelected = true;
  });
  BotToast.closeAllLoading();
}

问题原因

错误根源在于品牌下拉框的内部选中状态未被重置:

  • 切换地区时,仅清空了父组件的_brandStringList和_selectedBrand,但CustomDropdownWidget内部的_selectedOption仍保留上一次选中的品牌值。
  • 当新品牌列表加载后,若旧选中值不存在于新列表中,DropdownButton会因无法匹配到对应选项而抛出错误。

解决方案

方案一:通过Key强制重建品牌下拉框

修改品牌下拉框的Key,使其依赖选中的地区值,每次切换地区时强制组件重新初始化,自动清空内部选中状态:

AbsorbPointer(
  absorbing: !_isLocationSelected,
  child: CustomDropdownWidget<String>(
    // 用选中地区作为Key的一部分,触发组件重建
    key: Key('brand_dropdown_${_selectedLocation ?? ''}'),
    dropdownOptions: _brandStringList,
    onItemSelected: (String selectedValue) {
      print('Selected Value: $selectedValue');
      _setSelectedBrand(selectedValue.toString());
    },
    label: 'Brand',
  ),
),

方案二:给组件添加重置选中状态的方法

  1. 在父组件中定义GlobalKey:
final _brandDropdownKey = GlobalKey<_CustomDropdownWidgetState<String>>();
  1. 在_CustomDropdownWidgetState中添加重置方法:
class _CustomDropdownWidgetState<String>
    extends State<CustomDropdownWidget<String>> {
  String? _selectedOption;
  String? _label;

  // 添加重置选中状态的方法
  void resetSelectedOption() {
    setState(() {
      _selectedOption = null;
    });
  }

  // ... 其他代码不变
}
  1. 切换地区时主动调用重置方法:
void _getBrandsForLocation(String location) async {
  BotToast.showLoading(backButtonBehavior: BackButtonBehavior.none);
  _setSelectedLocation(location);

  // 先重置品牌下拉框的选中状态
  _brandDropdownKey.currentState?.resetSelectedOption();
  
  // 获取新品牌数据
  _brandList = await _repository!.getBrandsByID(_selectedLocation!);
  List<String> itemList = _brandList.map((e) => e.name!.text!).toList();

  setState(() {
    _brandStringList = itemList;
    _selectedBrand = null;
    _isLocationSelected = itemList.isNotEmpty;
  });
  BotToast.closeAllLoading();
}
  1. 给品牌下拉框绑定GlobalKey:
AbsorbPointer(
  absorbing: !_isLocationSelected,
  child: CustomDropdownWidget<String>(
    key: _brandDropdownKey,
    dropdownOptions: _brandStringList,
    onItemSelected: (String selectedValue) {
      print('Selected Value: $selectedValue');
      _setSelectedBrand(selectedValue.toString());
    },
    label: 'Brand',
  ),
),

内容的提问来源于stack exchange,提问作者GingerAle

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.07.12 17:35:54