Flutter DropdownButton空值异常:标记动画触发后报错排查
问题场景
使用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

