Flutter TabBar切换后DropdownButton断言错误修复求助
使用Flutter开发应用,通过TabBar实现标签页切换,其中一个页面用于选择城市和地区。当前问题:选择城市/地区后切换到其他标签页,返回该页面时触发断言错误:
Exception has occurred.
_AssertionError ('package:flutter/src/material/dropdown.dart': Failed assertion: line 1619 pos 15: 'items == null || items.isEmpty || value == null ||
items.where((DropdownMenuItemitem) {
return item.value == value;
}).length == 1': There should be exactly one item with [DropdownButton]'s value: ARTVİN.
Either zero or 2 or more [DropdownMenuItem]s were detected with the same value)
使用GetX进行状态管理,相关代码如下:
UI代码
// CityDropdown Obx(() => DropdownButtonHideUnderline( child: DropdownButtonFormField<City>( key: UniqueKey(), isExpanded: true, isDense: true, hint: Text('Lütfen İl seçiniz'), icon: Icon(Icons.arrow_forward_ios_sharp, color: AppColors.primary), decoration: InputDecoration( filled: true, fillColor: Colors.grey.shade200, hintStyle: TextStyle(), border: OutlineInputBorder( borderRadius: BorderRadius.circular(15), borderSide: BorderSide.none), alignLabelWithHint: true, ), value: customerCreateServiceRequestController.selectedCity.value, items: customerCreateServiceRequestController.cities .map<DropdownMenuItem<City>>((City city) { return DropdownMenuItem<City>( value: city, // Ensure this value is unique for each city. child: Text(city.name ?? ''), ); }).toList(), onChanged: (City? selectedCity) async { if (selectedCity != null) { customerCreateServiceRequestController.setSelectedCity(selectedCity); print('Selected City ID: ${selectedCity.id}'); print('Selected City NAME: ${selectedCity.name}'); // get district list await customerCreateServiceRequestController.fetchDistrict(); customerCreateServiceRequestController.selectCityName.value = selectedCity.name!; customerCreateServiceRequestController.selectCityID.value = selectedCity.id!; } }, ), )), // District Dropdown Obx(() => DropdownButtonHideUnderline( child: DropdownButtonFormField<District>( key: UniqueKey(), isExpanded: true, isDense: true, hint: Text('Lütfen İl seçiniz'), icon: Icon(Icons.arrow_forward_ios_sharp, color: AppColors.primary), decoration: InputDecoration( filled: true, fillColor: Colors.grey.shade200, hintStyle: TextStyle(), border: OutlineInputBorder( borderRadius: BorderRadius.circular(15), borderSide: BorderSide.none), alignLabelWithHint: true, ), value: customerCreateServiceRequestController.selectedDistrict.value, items: customerCreateServiceRequestController.districts .map<DropdownMenuItem<District>>((District district) { return DropdownMenuItem<District>( value: district, // Ensure this value is unique for each city. child: Text(district.name ?? ''), ); }).toList(), onChanged: (District? selectedDistrict) async { if (selectedDistrict != null) { print('Selected District ID: ${selectedDistrict.id}'); print('Selected District NAME: ${selectedDistrict.name}'); customerCreateServiceRequestController.setSelectedDistrict(selectedDistrict); } }, ), )),
API请求代码
Future<void> fetchCities() async { try { final response = await LocationApiService().fetchCityData(); if (response.statusCode == 200) { final CityResponseModel cityResponse = CityResponseModel.fromMap(response.body); cities.value = cityResponse.cities ?? []; } else { throw Exception('Failed to load cities'); } } catch (e) { print(e); } } Future<void> fetchDistrict() async { try { final selectedCityId = selectedCity.value?.id; if (selectedCityId != null) { final response = await LocationApiService().fetchDistrictData(selectedCityId); if (response.statusCode == 200) { final DistrictResponseModel districtResponse = DistrictResponseModel.fromMap(response.body); districts.value = districtResponse.districts ?? []; print(districts.length); // İlçe listesi güncellendikten sonra seçili ilçeyi sıfırla selectedDistrict.value = null; } else { throw Exception('Failed to load districts'); } } } catch (e) { print(e); } }
数据模型代码
City Model
import 'dart:convert'; class CityResponseModel { final List<City>? cities; CityResponseModel({ this.cities, }); factory CityResponseModel.fromJson(String str) => CityResponseModel.fromMap(json.decode(str)); String toJson() => json.encode(toMap()); factory CityResponseModel.fromMap(Map<String, dynamic> json) => CityResponseModel( cities: json["cities"] == null ? [] : List<City>.from(json["cities"]!.map((x) => City.fromMap(x))), ); Map<String, dynamic> toMap() => { "cities": cities == null ? [] : List<dynamic>.from(cities!.map((x) => x.toMap())), }; } class City { final int? id; final String? name; final int? countryId; City({ this.id, this.name, this.countryId, }); @override String toString() { return name!; } factory City.fromJson(String str) => City.fromMap(json.decode(str)); String toJson() => json.encode(toMap()); factory City.fromMap(Map<String, dynamic> json) => City( id: json["id"], name: json["name"], countryId: json["country_id"], ); Map<String, dynamic> toMap() => { "id": id, "name": name, "country_id": countryId, }; }
District Model
import 'dart:convert'; class DistrictResponseModel { final List<District>? districts; DistrictResponseModel({ this.districts, }); factory DistrictResponseModel.fromJson(String str) => DistrictResponseModel.fromMap(json.decode(str)); String toJson() => json.encode(toMap()); factory DistrictResponseModel.fromMap(Map<String, dynamic> json) => DistrictResponseModel( districts: json["districts"] == null ? [] : List<District>.from( json["districts"]!.map((x) => District.fromMap(x))), ); Map<String, dynamic> toMap() => { "districts": districts == null ? [] : List<dynamic>.from(districts!.map((x) => x.toMap())), }; } class District { final int? id; final String? name; final int? cityId; District({ this.id, this.name, this.cityId, }); @override String toString() { return name!; } factory District.fromJson(String str) => District.fromMap(json.decode(str)); String toJson() => json.encode(toMap()); factory District.fromMap(Map<String, dynamic> json) => District( id: json["id"], name: json["name"], cityId: json["city_id"], ); Map<String, dynamic> toMap() => { "id": id, "name": name, "city_id": cityId, }; }
核心原因
DropdownButtonFormField要求选中的value必须在items列表中存在且唯一。返回页面时,selectedCity.value对应的City对象,和重新请求得到的cities列表中的City对象是不同实例,Dart默认通过引用比较对象,导致Dropdown认为当前value不在items里,或者出现"重复"匹配的情况。
解决方法
1. 为City和District类重写==运算符和hashCode
让Dart根据对象的唯一标识(id)来比较,而非引用。修改模型类:
City类修改:
class City { final int? id; final String? name; final int? countryId; City({ this.id, this.name, this.countryId, }); @override String toString() { return name!; } // 重写==运算符,用id判断对象是否相等 @override bool operator ==(Object other) => identical(this, other) || other is City && runtimeType == other.runtimeType && id == other.id; // 基于id生成hashCode @override int get hashCode => id.hashCode; // 原有代码... }
District类修改:
class District { final int? id; final String? name; final int? cityId; District({ this.id, this.name, this.cityId, }); @override String toString() { return name!; } @override bool operator ==(Object other) => identical(this, other) || other is District && runtimeType == other.runtimeType && id == other.id; @override int get hashCode => id.hashCode; // 原有代码... }
2. 优化页面返回时的状态同步(可选)
如果返回页面时会重新调用fetchCities(),可以在请求完成后,根据保存的selectCityID重新匹配选中的City实例:
Future<void> fetchCities() async { try { final response = await LocationApiService().fetchCityData(); if (response.statusCode == 200) { final CityResponseModel cityResponse = CityResponseModel.fromMap(response.body); cities.value = cityResponse.cities ?? []; // 新增:重新匹配选中的城市 if (selectCityID.value != null) { selectedCity.value = cities.value.firstWhere( (city) => city.id == selectCityID.value, orElse: () => City(), // 或设置为null,根据业务调整 ); } } else { throw Exception('Failed to load cities'); } } catch (e) { print(e); } }
3. 移除不必要的UniqueKey(可选)
当前Dropdown使用了UniqueKey(),会导致每次重建时生成新Key,可能引发额外状态问题。如果上述方法生效,可考虑移除该Key,保持Dropdown状态稳定。
内容的提问来源于stack exchange,提问作者Abdullah T

