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

Flutter DropdownButton空值异常:标记动画触发后报错排查

Flutter DropdownButton绑定Marker动画时断言异常问题

问题场景

使用DropdownButton绑定地图Marker实现标记选择功能,选中标记后可正常定位到对应位置,但点击方向按钮触发Marker沿折线点移动的动画时,触发Widgets库断言异常,提示DropdownButton的value对应的DropdownMenuItem数量不为1。

异常详情

========= Exception caught by widgets library =======================================================
The following assertion was thrown building MyHomePage(dirty, dependencies: [MediaQuery], state: _MyHomePageState#a3bdf(tickers: tracking 1 ticker)):
There should be exactly one item with [DropdownButton]'s value: Marker{markerId: MarkerId(First Marker), alpha: 1.0, anchor: Offset(0.5, 1.0), consumeTapEvents: false, draggable: false, flat: false, icon: Instance of 'BitmapDescriptor', infoWindow: InfoWindow{title: First Marker, snippet: null, anchor: Offset(0.5, 0.0)}, position: LatLng(37.42267, -122.0838), rotation: 0.0, visible: true, zIndex: 0.0, onTap: null, onDragStart: null, onDrag: null, onDragEnd: null}.
Either zero or 2 or more [DropdownMenuItem]s were detected with the same value
'package:flutter/src/material/dropdown.dart':
Failed assertion: line 888 pos 15:
'items == null || items.isEmpty || value == null || items.where((DropdownMenuItem<T> item) { return item.value == value; }).length == 1'

出错的DropdownButton代码

DropdownButton(
  value: selectedMarker,
  onChanged: ((Marker? marker) {
    setState(() {
      selectedMarker = marker!;
    });
    print("selectedmarker value ${selectedMarker}");
    mapController.moveCamera(CameraUpdate.newLatLng(marker!.position));
  }),
  items: markers.map((marker) {
    return DropdownMenuItem(
      value: marker,
      child: Text(marker.markerId.value),
    );
  }).toList(),
),

触发异常的方向按钮代码

IconButton(
  onPressed: () async {
    if(selectedMarker.markerId == MarkerId("First Marker")){
      controller = AnimationController(
        duration: const Duration(milliseconds: 1000),
        vsync: this,
      );
      selectedMarker = markers[0];
      for (int i = 0; i < polylinePoints.length && i + 1 != polylinePoints.length; i = i + 1) {
        markerAnimation = LatLngTween(
          begin: polylinePoints[i],
          end: polylinePoints[i + 1])
          .animate(controller);
        markerAnimation.addListener(() {
          setState(() {
            Marker marker1 = Marker(markerId: MarkerId("First Marker"),
              icon:BitmapDescriptor.defaultMarkerWithHue(BitmapDescriptor.hueGreen),
            );
            print("This is marker1 ${marker1}");
            marker1 = marker1.copyWith(
              positionParam: markerAnimation.value,
            );
            markers[0] = marker1;
          });
        });
        controller.forward();
        await Future.delayed(
          const Duration(milliseconds: 50));
      }
    }
    if(selectedMarker.markerId == MarkerId("Second Marker")){
      controller = AnimationController(
        duration: const Duration(milliseconds: 1000),
        vsync: this,
      );
      selectedMarker = markers[1];
      for (int i = 0; i < polylinePoints1.length && i + 1 != polylinePoints1.length; i = i + 1) {
        markerAnimation = LatLngTween(
          begin: polylinePoints1[i],
          end: polylinePoints1[i + 1])
          .animate(controller);
        markerAnimation.addListener(() {
          setState(() {
            Marker marker1 = Marker(markerId: MarkerId("Second Marker"),
              icon: BitmapDescriptor.defaultMarkerWithHue(BitmapDescriptor.hueGreen),
            );
            marker1 = marker1.copyWith(
              positionParam: markerAnimation.value,
            );
            markers[1] = marker1;
          });
        });
        controller.forward();
        await Future.delayed(
          const Duration(milliseconds: 50));
      }
    }
    if(selectedMarker.markerId == MarkerId("Third Marker")){
      controller = AnimationController(
        duration: const Duration(milliseconds: 1000),
        vsync: this,
      );
      selectedMarker = markers[2];
      for (int i = 0; i < polylinePoints2.length && i + 1 != polylinePoints2.length; i = i + 1) {
        markerAnimation = LatLngTween(
          begin: polylinePoints2[i],
          end: polylinePoints2[i + 1])
          .animate(controller);
        markerAnimation.addListener(() {
          setState(() {
            Marker marker1 = Marker(markerId: MarkerId("Third Marker"),
              icon: BitmapDescriptor.defaultMarkerWithHue(BitmapDescriptor.hueGreen),
            );
            marker1 = marker1.copyWith(
              positionParam: markerAnimation.value,
            );
            markers[2] = marker1;
          });
        });
        controller.forward();
        await Future.delayed(
          const Duration(milliseconds: 50));
      }
    }
  },
  icon: Icon(Icons.directions))

问题原因

核心问题在于Marker对象的相等性判断逻辑:

  • 动画过程中,每次都会创建新的Marker对象替换markers列表中的对应元素
  • DropdownButton的value持有动画前的旧Marker对象引用,而items里的Marker已经被替换为新对象
  • Flutter默认通过对象引用判断Marker是否相等,而非markerId,导致DropdownButton无法找到与selectedMarker匹配的MenuItem,触发断言

解决方法

方案1:使用MarkerId作为Dropdown的value(推荐)

放弃直接用Marker对象作为Dropdown的value,改用唯一标识markerId.value字符串,确保无论Marker对象如何更新,只要id不变,Dropdown就能正确匹配:

修改DropdownButton代码

// 将selectedMarker改为String类型,存储markerId的value
String? selectedMarkerId;

DropdownButton<String>(
  value: selectedMarkerId,
  onChanged: ((String? id) {
    setState(() {
      selectedMarkerId = id;
      // 根据id找到对应的Marker对象
      selectedMarker = markers.firstWhere((m) => m.markerId.value == id);
    });
    mapController.moveCamera(CameraUpdate.newLatLng(selectedMarker.position));
  }),
  items: markers.map((marker) {
    return DropdownMenuItem<String>(
      value: marker.markerId.value,
      child: Text(marker.markerId.value),
    );
  }).toList(),
),

修改方向按钮的判断逻辑

// 改为基于selectedMarkerId判断
if(selectedMarkerId == "First Marker"){
  // 原有动画逻辑保持不变
}

方案2:重写Marker的相等性判断(不推荐,实现复杂)

如果必须使用Marker对象作为Dropdown的value,需要自定义Marker类并重写==和hashCode方法,让它基于markerId判断相等:

class CustomMarker extends Marker {
  CustomMarker({required MarkerId markerId, super.icon, super.position, super.infoWindow}) 
      : super(markerId: markerId);

  @override
  bool operator ==(Object other) =>
      identical(this, other) ||
      other is CustomMarker &&
          runtimeType == other.runtimeType &&
          markerId == other.markerId;

  @override
  int get hashCode => markerId.hashCode;
}

后续所有Marker都使用CustomMarker创建,这样即使对象引用变化,只要markerId相同,就会被判定为相等,Dropdown即可正常匹配。


内容的提问来源于stack exchange,提问作者Md. Samiul Islam

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.07.22 04:52:49