Flutter DropdownButtonFormField2重复值错误及联动重置需求
我用两个联动的Dropdown组件:先选第一个下拉框的城市,加载对应门店数据后在第二个下拉框展示。首次使用正常,但选中城市和门店后切换城市时,门店下拉框报错:
There should be exactly one item with [DropdownButtonFormField]'s value: Instance of 'OutletList'.
Either zero or 2 or more [DropdownMenuItem]s were detected with the same value
'package:dropdown_button2/dropdown_button2.dart':
Failed assertion: line 1223 pos 11: 'items == null ||
items.isEmpty ||
value == null ||
items.where((DropdownMenuItemitem) {
return item.value == value;
}).length ==
1'
期望切换城市时自动清空已选的门店数据,避免该错误。以下是我的代码:
import 'package:dropdown_button2/dropdown_button2.dart'; import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'dart:ui'; import '../../BlocPages/CubitPage/OutletCubit/outlet_cubit.dart'; import '../../BlocPages/CubitPage/OutletCubit/outlet_state.dart'; import '../../Models/outlet_city_list_model.dart'; import '../../Models/outlet_list_model.dart'; import '../ConstantsPages/toast_page.dart'; import '../GlobalColor/global_color.dart'; import '../GlobalTextStyles/global_text_styles.dart'; import '../GlobalWidgetPages/center_text_with_container.dart'; import '../GlobalWidgetPages/double_btn_in_single_row.dart'; class OutletDialogPage extends StatefulWidget { final String title; final List<OutletCityListModel> data; const OutletDialogPage({ Key? key, required this.title, required this.data, }) : super(key: key); @override State<OutletDialogPage> createState() => _OutletDialogPageState(); } class _OutletDialogPageState extends State<OutletDialogPage> { List<OutletCityListModel> cityList = []; List<OutletList> outletList = []; String outletAddress = ""; String outletName = ""; List<OutletListModel>? selectedData; @override void initState() { print(selectedData); print("my data ${widget.data.length}"); super.initState(); cityList = widget.data.toList(); } @override Widget build(BuildContext context) { return Dialog( insetPadding: EdgeInsets.all(20), shape: RoundedRectangleBorder( borderRadius: BorderRadius.circular(10), ), elevation: 0.0, backgroundColor: Colors.transparent, child: dialogContent(context), ); } } double t4Size = 14; extension widgetExtension on _OutletDialogPageState { dialogContent(BuildContext context) { return BackdropFilter( filter: ImageFilter.blur(sigmaX: 3.3, sigmaY: 3.3), child: Dialog( insetPadding: EdgeInsets.only(top: 30, bottom: 30, left: 0, right: 0), shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(15.0)), // backgroundColor: skyBlue, child: ClipRRect( borderRadius: BorderRadius.circular(10), child: Container( decoration: new BoxDecoration( color: MyColor.whiteColor, shape: BoxShape.rectangle, boxShadow: [ BoxShadow( color: MyColor.blackColor.withOpacity(0.2), blurRadius: 10.0, offset: const Offset(0.0, 10.0), ), ], ), child: Column( crossAxisAlignment: CrossAxisAlignment.center, mainAxisSize: MainAxisSize.min, children: [ Container( decoration: new BoxDecoration( color: MyColor.primaryRedColor, shape: BoxShape.rectangle, borderRadius: BorderRadius.only( topLeft: Radius.circular(10), topRight: Radius.circular(10), ), ), child: CenterTextWithContainer(txt: "${widget.title}"), ), Container( child: Column( children: [ Padding( padding: const EdgeInsets.only( top: 23.0, left: 20, right: 20, bottom: 0), child: DropdownButtonFormField2( itemSplashColor: MyColor.lightBlueColor, itemHeight: 36, itemHighlightColor: MyColor.greyColor2, dropdownMaxHeight: 200, dropdownPadding: EdgeInsets.only(left: 2, right: 2), selectedItemHighlightColor: MyColor.lightBlueColor, decoration: InputDecoration( focusedBorder: OutlineInputBorder( borderSide: BorderSide(color: MyColor.darkGreyColor), borderRadius: BorderRadius.circular(8.0), ), isDense: true, contentPadding: EdgeInsets.zero, border: OutlineInputBorder( borderRadius: BorderRadius.circular(8), ), ), isExpanded: true, hint: Row( children: [ Container( height: 18, width: 18, decoration: const BoxDecoration( image: DecorationImage( image: AssetImage( 'assets/outline_location_pin.png'), fit: BoxFit.fill, )), ), SizedBox( width: 4, ), Expanded( child: Text( 'Select City', style: textStyleWith12400( MyColor.darkGreyColor), maxLines: 1, overflow: TextOverflow.ellipsis, ), ), ], ), icon: const Icon( Icons.arrow_drop_down, color: MyColor.darkGreyColor, ), // iconOnClick: Icon( // Icons.arrow_drop_up, // color: MyColor.darkGreyColor, // ), iconSize: 22, buttonHeight: 44, buttonPadding: const EdgeInsets.only(left: 0, right: 12), dropdownDecoration: BoxDecoration( borderRadius: BorderRadius.circular(8), ), items: cityList .map<DropdownMenuItem<OutletCityListModel>>( (OutletCityListModel option) { return DropdownMenuItem( value: option, child: Text( option.city.toString(), style: textStyleWith12400(MyColor.blackColor), )); }).toList(), selectedItemBuilder: (con) { return cityList.map<Widget>((item) { return Row( children: [ Container( height: 18, width: 18, decoration: const BoxDecoration( image: DecorationImage( image: AssetImage( 'assets/outline_location_pin.png'), fit: BoxFit.fill, )), ), SizedBox( width: 4, ), Expanded( child: Text( "${item.city}", style: textStyleWith12400( MyColor.blackColor), maxLines: 1, overflow: TextOverflow.ellipsis, ), ) ], ); }).toList(); }, onChanged: (OutletCityListModel? value) { print("cities:: " + value.toString()); setState(() { outletList.clear(); selectedData = null; outletName = ""; outletAddress = ""; outletList = value!.outletList!; print("outletList:: " + outletList.length.toString()); }); //Do something when changing the item if you want. }, ), ), Padding( padding: const EdgeInsets.only( top: 12.0, left: 20, right: 20, bottom: 0), child: DropdownButtonFormField2( itemSplashColor: MyColor.lightBlueColor, itemHeight: 36, itemHighlightColor: MyColor.greyColor2, dropdownMaxHeight: 200, dropdownPadding: EdgeInsets.only(left: 2, right: 2), selectedItemHighlightColor: MyColor.lightBlueColor, decoration: InputDecoration( focusedBorder: OutlineInputBorder( borderSide: BorderSide(color: MyColor.darkGreyColor), borderRadius: BorderRadius.circular(8.0), ), isDense: true, contentPadding: EdgeInsets.zero, border: OutlineInputBorder( borderRadius: BorderRadius.circular(8), ), ), isExpanded: true, hint: Row( children: [ Image.asset( "assets/current_outlet_icon.png", height: 14.81, width: 18, color: MyColor.greyColor, ), SizedBox( width: 4, ), Expanded( child: Text( 'Select Outlet', style: textStyleWith12400( MyColor.darkGreyColor), maxLines: 1, overflow: TextOverflow.ellipsis, ), ), ], ), icon: const Icon( Icons.arrow_drop_down, color: MyColor.darkGreyColor, ), // iconOnClick: Icon( // Icons.arrow_drop_up, // color: MyColor.darkGreyColor, // ), iconSize: 22, buttonHeight: 44, buttonPadding: const EdgeInsets.only(left: 0, right: 12), dropdownDecoration: BoxDecoration( borderRadius: BorderRadius.circular(8), ), items: outletList .map<DropdownMenuItem<OutletList>>((option) { return DropdownMenuItem( value: option, child: Text( option.title.toString(), style: textStyleWith12400(MyColor.blackColor), )); }).toList(), selectedItemBuilder: (con) { return outletList.map<Widget>((item) { return Row( children: [ Image.asset( "assets/current_outlet_icon.png", height: 14.81, width: 18, color: MyColor.greyColor, ), SizedBox( width: 4, ), Expanded( child: Text( "${item.title}", style: textStyleWith12400( MyColor.blackColor), maxLines: 1, overflow: TextOverflow.ellipsis, ), ) ], ); }).toList(); }, onChanged: (OutletList? value) { print("outlet" + value!.toJson().toString()); setState(() { selectedData = [OutletListModel.fromJson(value.toJson())]; outletName = "${value.title}"; outletAddress = "${value.address}"; print("selectedData $selectedData"); }); //Do something when changing the item if you want. }, ), ), Padding( padding: const EdgeInsets.only( top: 21.0, left: 20, right: 20, bottom: 30), child: Container( // height: 134, child: outletAddress == "" ? Container() : Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( "Outlet Address", style: textStyleWith16600( MyColor.blackColor), ), SizedBox( height: 16, ), Container( padding: const EdgeInsets.only( top: 15.0, left: 12, right: 12, bottom: 13), decoration: BoxDecoration( color: MyColor.whiteColor, borderRadius: BorderRadius.circular(8), boxShadow: [ BoxShadow( color: MyColor.greyColor .withOpacity(0.3), blurRadius: 3.0, ), ], ), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Row( crossAxisAlignment: CrossAxisAlignment.start, children: [ Padding( padding: EdgeInsets.only(top: 2), child: Image.asset( "assets/current_outlet_icon.png", height: 15, width: 17.30, color: MyColor.blueColor, ), ), SizedBox( width: 8, ), Expanded( child: Text( "$outletName", style: textStyleWith14500( MyColor.blackColor), maxLines: 1, overflow: TextOverflow.ellipsis, ), ) ], ), Text( "$outletAddress", style: textStyleWith10400( MyColor.darkGreyColor), maxLines: 3, overflow: TextOverflow.ellipsis, ) ], ), ), ], ), ), ) ], ), ), DoubleBtnInSingleRow( cancelOnPressed: () { Navigator.pop(context); }, cancelText: "CANCEL", successText: "SUBMIT", successOnPressed: () { print("selectedData $selectedData"); if (selectedData != null) { BlocProvider.of<OutletCubit>(context) .emit(OutletCodeSentState(selectedData!)); Navigator.pop(context); } else { showToastWithBlackColor( "Please select your city and outlet."); } }, ) ], )), ), ), ); } }
报错本质是:切换城市后,门店下拉框的选中值(之前选的门店实例)不在新加载的outletList里,Dropdown找不到对应选项,触发了组件的断言检查。你之前只清空了selectedData,但没明确重置门店Dropdown的选中状态。
修改点:
- 在
_OutletDialogPageState类中新增变量,专门跟踪门店的选中状态:
OutletList? selectedOutlet;
- 给门店Dropdown绑定
value属性,关联到selectedOutlet:
DropdownButtonFormField2( value: selectedOutlet, // 新增这行 // ...其他原有属性 )
- 切换城市时,在
setState里清空selectedOutlet:
onChanged: (OutletCityListModel? value) { setState(() { outletList.clear(); selectedData = null; outletName = ""; outletAddress = ""; selectedOutlet = null; // 新增这行,清空选中的门店 outletList = value!.outletList!; }); }
- 选择门店时,更新
selectedOutlet为当前选中的实例:
onChanged: (OutletList? value) { setState(() { selectedOutlet = value; // 新增这行 selectedData = [OutletListModel.fromJson(value!.toJson())]; outletName = "${value.title}"; outletAddress = "${value.address}"; }); }
这样切换城市后,门店Dropdown的选中值被重置为null,不会再试图匹配旧的门店实例,就能彻底避免断言错误。
内容的提问来源于stack exchange,提问作者Deepak

