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

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(),
            ),
          ),
          ),

异常原因分析

  1. Value不匹配:选中下拉选项后,dropdownValue被设置为newValue + ":8100",但下拉菜单的所有DropdownMenuItem的value都是不带端口的IP地址,导致DropdownButton无法找到对应value的选项,触发断言检查失败。
  2. FutureBuilder重复执行:build方法中每次都会创建新的scanNetwork() Future,导致网络扫描重复执行,频繁修改hostIpList,可能引发状态不一致。
  3. 初始选项丢失风险:网络扫描后的去重操作可能会误删初始的"Seleziona IP"选项,导致DropdownButton的初始value找不到对应选项。
  4. 频繁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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.07.12 17:44:53