Flutter中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', ), ),
方案二:给组件添加重置选中状态的方法
- 在父组件中定义GlobalKey:
final _brandDropdownKey = GlobalKey<_CustomDropdownWidgetState<String>>();
- 在
_CustomDropdownWidgetState中添加重置方法:
class _CustomDropdownWidgetState<String> extends State<CustomDropdownWidget<String>> { String? _selectedOption; String? _label; // 添加重置选中状态的方法 void resetSelectedOption() { setState(() { _selectedOption = null; }); } // ... 其他代码不变 }
- 切换地区时主动调用重置方法:
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(); }
- 给品牌下拉框绑定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
相关产品推荐
相关产品推荐

