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

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((DropdownMenuItem item) {
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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.06.29 21:40:54