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

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.08.04 05:30:49