Flutter连接ESP32后新增Tab无法显示设备信息及数据求助
问题诊断与修复方案
你遇到的核心问题是TabController与TabBarView的数据源不同步,以及双重Tab控制器冲突导致的内容不渲染,以下是具体问题点和修复步骤:
1. 双重Tab控制器冲突
代码同时使用了DefaultTabController和自定义的_tabController,导致Flutter无法正确关联TabBar和TabBarView的状态。必须移除DefaultTabController,完全使用自定义的_tabController控制Tab切换。
2. TabController长度未动态更新
添加/移除设备时仅更新了tabs列表,但未重新初始化_tabController的长度,导致TabBarView的子组件数量与TabBar标签数量不匹配,新添加的Tab没有对应内容视图。
3. 初始空Tab与数据源不一致
初始tabs列表包含空Tab,但_stations列表为空,导致TabBar有1个标签但TabBarView无对应子组件,触发渲染错误。应让tabs从_stations动态生成,避免静态列表不同步。
4. WebSocket连接未独立管理
单个_channel变量会被新设备连接覆盖,导致旧设备连接丢失。需给每个Station实例单独维护WebSocket连接。
5. 移除设备时未同步更新_stations
当前移除操作仅删除tabs中的项,未同步删除_stations中对应的Station,导致数据与视图不同步。
完整修正代码
import 'dart:convert'; import 'package:flutter/material.dart'; import 'package:web_socket_channel/io.dart'; void main() { runApp(const MyApp()); } class MyApp extends StatelessWidget { const MyApp({super.key}); @override Widget build(BuildContext context) { return const MaterialApp( debugShowCheckedModeBanner: false, title: 'ESP32 Monitor', home: MyHomePage(), ); } } class MyHomePage extends StatefulWidget { const MyHomePage({super.key}); @override MyHomePageState createState() => MyHomePageState(); } class Station { final String name; final String ipAddress; final String port; double rangeData; double forceData; IOWebSocketChannel? channel; // 每个设备独立维护连接 Station({ required this.name, required this.ipAddress, required this.port, this.rangeData = 0.0, this.forceData = 0.0, this.channel, }); } class MyHomePageState extends State<MyHomePage> with SingleTickerProviderStateMixin { final GlobalKey<FormState> _formKey = GlobalKey<FormState>(); final List<Station> _stations = []; late TabController _tabController; late TextEditingController _stationName; late TextEditingController _ipAddress; late TextEditingController _port; String? selectedTabName; // 动态生成Tabs,避免静态列表不同步 List<Tab> get _tabs => _stations.map((station) => Tab(text: station.name)).toList(); @override void initState() { _stationName = TextEditingController(); _ipAddress = TextEditingController(); _port = TextEditingController(); _tabController = TabController(vsync: this, length: _tabs.length); super.initState(); } @override void dispose() { // 关闭所有WebSocket连接 for (var station in _stations) { station.channel?.sink.close(); } _stationName.dispose(); _ipAddress.dispose(); _port.dispose(); _tabController.dispose(); super.dispose(); } void _connectToEsp32(Station station) { // 先关闭旧连接(如果存在) station.channel?.sink.close(); station.channel = IOWebSocketChannel.connect('ws://${station.ipAddress}:${station.port}'); station.channel?.stream.listen( (data) { final Map<String, dynamic> receivedData = jsonDecode(data); setState(() { station.rangeData = receivedData['range']; station.forceData = receivedData['force']; }); }, onError: (error) { print('连接错误:$error'); }, onDone: () { print('连接已关闭'); }, ); } void _openAddStationDialog(BuildContext context) { _stationName.text = ""; _ipAddress.text = ""; _port.text = ""; showDialog( context: context, builder: (BuildContext context) { return AlertDialog( title: const Text('Add Station'), content: Form( key: _formKey, child: Column( mainAxisSize: MainAxisSize.min, children: <Widget>[ TextFormField( decoration: const InputDecoration( suffixIcon: Icon(Icons.list), labelText: 'Station Name', ), controller: _stationName, validator: (value) { if (value?.isEmpty ?? true) { return 'Please enter a Station Name'; } return null; }, ), TextFormField( decoration: const InputDecoration( suffixIcon: Icon(Icons.computer), labelText: 'IP Address', ), controller: _ipAddress, validator: (value) { if (value?.isEmpty ?? true) { return 'Please enter IP address'; } return null; }, ), TextFormField( decoration: const InputDecoration( suffixIcon: Icon(Icons.settings_applications), labelText: 'Port', ), controller: _port, validator: (value) { if (value?.isEmpty ?? true) { return 'Please enter Port'; } return null; }, ), ], ), ), actions: <Widget>[ ElevatedButton.icon( onPressed: () { if (_formKey.currentState!.validate()) { setState(() { final station = Station( name: _stationName.text, ipAddress: _ipAddress.text, port: _port.text, ); _stations.add(station); // 更新TabController长度 _tabController.dispose(); _tabController = TabController(vsync: this, length: _tabs.length); _connectToEsp32(station); }); Navigator.pop(context); } }, icon: const Icon(Icons.add), label: const Text('Add'), ), ], ); }, ); } void _removeStation() { if (selectedTabName == null) return; setState(() { final index = _stations.indexWhere((s) => s.name == selectedTabName); if (index >= 0) { // 关闭连接并移除设备 _stations[index].channel?.sink.close(); _stations.removeAt(index); // 更新TabController _tabController.dispose(); _tabController = TabController(vsync: this, length: _tabs.length); selectedTabName = _stations.isNotEmpty ? _stations[0].name : null; } }); } @override Widget build(BuildContext context) { return Scaffold( appBar: AppBar( title: const Text('Station Monitor'), actions: <Widget>[ ElevatedButton.icon( onPressed: () => _openAddStationDialog(context), icon: const Icon(Icons.keyboard_control_rounded), label: const Text('Add Station'), ), const SizedBox(width: 8), ElevatedButton.icon( onPressed: _stations.isEmpty ? null : () { showDialog( context: context, builder: (BuildContext context) { return AlertDialog( title: const Text("Select tab to remove"), content: StatefulBuilder(builder: (BuildContext context, StateSetter setState) { return DropdownButton<String>( items: _stations.map((station) { return DropdownMenuItem<String>( value: station.name, child: Text(station.name), ); }).toList(), onChanged: (String? value) { setState(() { selectedTabName = value; }); }, value: selectedTabName ?? _stations.first.name, ); }), actions: <Widget>[ ElevatedButton( child: const Text("Remove"), onPressed: () { _removeStation(); Navigator.of(context).pop(); }, ), ], ); }, ); }, icon: const Icon(Icons.remove), label: const Text('Remove Station'), ), ], bottom: _tabs.isNotEmpty ? TabBar( controller: _tabController, tabs: _tabs, ) : null, ), body: _stations.isEmpty ? const Center(child: Text("Please add a station to start monitoring")) : TabBarView( controller: _tabController, children: _stations.map((station) { return Column( children: <Widget>[ const SizedBox(height: 30), Row( mainAxisAlignment: MainAxisAlignment.center, children: <Widget>[ Text("Station Name: ${station.name}"), const SizedBox(width: 10), PopupMenuButton<int>( itemBuilder: (context) => [ const PopupMenuItem( value: 1, child: Text("Reconnect"), ), ], onSelected: (value) { if (value == 1) { _connectToEsp32(station); } }, ), ], ), const SizedBox(height: 20), Text("Range: ${station.rangeData.toStringAsFixed(2)}"), const SizedBox(height: 10), Text("Force: ${station.forceData.toStringAsFixed(2)}"), ], ); }).toList(), ), ); } }
关键修复点说明
- 移除
DefaultTabController,完全使用自定义_tabController,并在设备添加/移除时重新初始化控制器长度 - 让Tabs从
_stations动态生成,确保标签与数据源完全同步 - 给每个
Station添加独立的channel字段,避免连接被覆盖 - 移除设备时同步删除
_stations中的数据并关闭对应WebSocket连接 - 优化初始状态显示,当没有设备时展示提示文本
- 增加WebSocket连接的错误和关闭监听,提升稳定性
内容的提问来源于stack exchange,提问作者Carlos Costa
相关产品推荐
相关产品推荐

