Flutter DropdownButton断言失败异常排查与解决
异常信息
There should be exactly one item with [DropdownButton]'s value: 172.16.54.22:8100. Either zero or 2 or more [DropdownMenuItem]s were detected with the same value 'package:flutter/src/material/dropdown.dart': Failed assertion: line 892 pos 15: 'items == null || items.isEmpty || value == null || items.where((DropdownMenuItem<T> item) { return item.value == value; }).length == 1'
问题代码
class LoginPage extends KFDrawerContent { @override State<LoginPage> createState() => _LoginPageState(); } class _LoginPageState extends State<LoginPage> { TextEditingController _email = TextEditingController(text: "http://207.154.221.255:8100"); final TextStyle textStyle = TextStyle(color: Colors.white); List<String> hostIpList = ["Seleziona IP"]; String dropdownValue = "Seleziona IP"; @override Widget build(BuildContext context) { return FutureBuilder<bool>( future: scanNetwork(), builder: (BuildContext context, AsyncSnapshot<bool> snapshot) { if (snapshot.hasError) { return Container(); } else { return Scaffold( backgroundColor: primary, body: getBody(), ); } }); } Future<bool> scanNetwork() async { await (NetworkInfo().getWifiIP()).then( (ip) async { final String subnet = ip.substring(0, ip.lastIndexOf('.')); const port = 22; for (var i = 0; i < 256; i++) { String ip = '$subnet.$i'; await Socket.connect(ip, port, timeout: Duration(milliseconds: 50)) .then((socket) async { await InternetAddress(socket.address.address) .reverse() .then((value) { setState(() { hostIpList.add(value.address); hostIpList = Verifica.uniqueArray(hostIpList); }); }).catchError((error) { setState(() { var ip = socket.address.host.toString().replaceAll( ", IPv4)", ""); hostIpList.add(socket.address.address); hostIpList = Verifica.uniqueArray(hostIpList); }); }); socket.destroy(); }).catchError((error) => null); } }, ); return true; } Future<String> alert(BuildContext context, String testo) { return showDialog<String>( context: context, builder: (BuildContext context) => AlertDialog( title: const Text( 'ATTENZIONE', style: TextStyle(color: Colors.red), ), content: Text(testo), actions: <Widget>[ Center( child: TextButton( style: TextButton.styleFrom( foregroundColor: Colors.white, backgroundColor: Colors.red, // Background Color ), onPressed: () => Navigator.pop(context, 'Cancel'), child: const Text('OK'), ), ), ], ), ); } void login(BuildContext context) async { if (_email.text.length > 0) { Storage.salva("URL", _email.text); Macchina mc = await Macchina.getModel(); if (mc.model.length > 0) { Storage.salva("Model", mc.model); Storage.salva("Serial", mc.serial); Storage.salva("Time", mc.time); await Cloud.getToken(); Navigator.pushReplacement( context, MaterialPageRoute( builder: (context) => HomePage(), )); } else { alert(context, "Connessione non riuscita, verifica l'indirizzo ip e di essere connesso al raspberry pi "); } } else { alert(context, "URL non valido"); } } Widget getBody() { return SafeArea( child: Center( child: Column( children: [ const SizedBox( height: 50, ), Container( width: 70, height: 70, decoration: const BoxDecoration( shape: BoxShape.circle, image: DecorationImage( image: NetworkImage( "https://yt3.googleusercontent.com/ytc/AL5GRJUCIV0j7t9jSOt5Mq87622YCHl_hqs7dMlgh4MY=s900-c-k-c0x00ffffff-no-rj"), fit: BoxFit.cover)), ), const SizedBox( height: 30, ), const Text("Roboqbo S.R.L"), const SizedBox( height: 30, ), FittedBox( fit: BoxFit.contain, child: Center( child: DropdownButton<String>( value: dropdownValue, onChanged: (String newValue) { setState(() { _email.text = newValue + ":8100"; dropdownValue = newValue + ":8100"; }); }, items: hostIpList.map<DropdownMenuItem<String>>((String value) { return DropdownMenuItem<String>( value: value, child: Text(value), ); }).toList(), ), ), ),
异常原因分析
- Value不匹配:选中下拉选项后,
dropdownValue被设置为newValue + ":8100",但下拉菜单的所有DropdownMenuItem的value都是不带端口的IP地址,导致DropdownButton无法找到对应value的选项,触发断言检查失败。 - FutureBuilder重复执行:
build方法中每次都会创建新的scanNetwork()Future,导致网络扫描重复执行,频繁修改hostIpList,可能引发状态不一致。 - 初始选项丢失风险:网络扫描后的去重操作可能会误删初始的"Seleziona IP"选项,导致DropdownButton的初始value找不到对应选项。
- 频繁setState:网络扫描循环中每次添加IP都调用
setState,不仅影响性能,还可能导致列表状态频繁变化,增加异常概率。
解决办法
1. 修正DropdownButton的Value匹配逻辑
修改onChanged回调,保持dropdownValue与DropdownMenuItem的value一致,仅在设置输入框文本时追加端口:
onChanged: (String newValue) { setState(() { _email.text = newValue + ":8100"; dropdownValue = newValue; // 不再给dropdownValue追加端口 }); },
2. 避免FutureBuilder重复执行网络扫描
将网络扫描的Future存储为成员变量,仅在初始化时执行一次:
Future<bool>? _scanFuture; @override void initState() { super.initState(); _scanFuture = scanNetwork(); // 初始化时启动扫描 } @override Widget build(BuildContext context) { return FutureBuilder<bool>( future: _scanFuture, // 使用已初始化的Future builder: (BuildContext context, AsyncSnapshot<bool> snapshot) { if (snapshot.connectionState == ConnectionState.waiting) { return Center(child: CircularProgressIndicator()); // 添加加载提示 } else if (snapshot.hasError) { return Center(child: Text("网络扫描出错")); } else { return Scaffold( backgroundColor: primary, body: getBody(), ); } }, ); }
3. 确保初始选项始终存在
在网络扫描的去重操作后,检查并保留初始的"Seleziona IP"选项:
// 在scanNetwork的then回调中修改setState部分 setState(() { hostIpList.add(value.address); hostIpList = Verifica.uniqueArray(hostIpList); // 确保初始选项不丢失 if (!hostIpList.contains("Seleziona IP")) { hostIpList.insert(0, "Seleziona IP"); } });
同样在catchError的setState块中执行相同的检查逻辑。
4. 优化网络扫描的状态更新
先收集所有扫描到的IP,最后一次性更新状态,减少setState调用次数:
Future<bool> scanNetwork() async { final ip = await NetworkInfo().getWifiIP(); final String subnet = ip.substring(0, ip.lastIndexOf('.')); const port = 22; List<String> tempList = ["Seleziona IP"]; // 临时列表存储结果 for (var i = 0; i < 256; i++) { String ip = '$subnet.$i'; try { final socket = await Socket.connect(ip, port, timeout: Duration(milliseconds: 50)); try { final value = await InternetAddress(socket.address.address).reverse(); tempList.add(value.address); } catch (error) { tempList.add(socket.address.address); } socket.destroy(); } catch (error) { // 连接失败忽略 } } // 去重并确保初始选项存在 tempList = Verifica.uniqueArray(tempList); if (!tempList.contains("Seleziona IP")) { tempList.insert(0, "Seleziona IP"); } setState(() { hostIpList = tempList; }); return true; }
内容的提问来源于stack exchange,提问作者riki
相关产品推荐
相关产品推荐

