Flutter--扫描BLE设备

效果图
架构图
Dart 复制代码
┌─────────────────────────────────────────────────────────┐
│                    UI层 (Presentation Layer)            │
├─────────────────────────────────────────────────────────┤
│  Widget树构建 │ 事件响应 │ 状态更新 │ 用户交互反馈        │
└─────────────────────────────────────────────────────────┘
                            ↓
┌─────────────────────────────────────────────────────────┐
│             业务逻辑层 (Business Logic Layer)           │
├─────────────────────────────────────────────────────────┤
│ 扫描管理 │ 设备管理 │ 连接管理 │ 权限管理 │ 状态管理     │
└─────────────────────────────────────────────────────────┘
                            ↓
┌─────────────────────────────────────────────────────────┐
│             数据访问层 (Data Access Layer)              │
├─────────────────────────────────────────────────────────┤
│  Bluetooth Adapter │ 扫描结果流 │ 设备操作 │ 平台适配   │
└─────────────────────────────────────────────────────────┘
                            ↓
┌─────────────────────────────────────────────────────────┐
│                原生层 (Native Layer)                    │
├─────────────────────────────────────────────────────────┤
│  FlutterBluePlus插件 │ Android/iOS原生蓝牙API │ 硬件     │
└─────────────────────────────────────────────────────────┘
扫描数据流
Dart 复制代码
蓝牙硬件 → 原生系统 → FlutterBluePlus插件 → Stream<ScanResult> → 监听器 → BleDeviceInfo → _devices Map → UI更新
     ↑           ↑            ↑                 ↑          ↑            ↑            ↑            ↑
  信号强度    设备信息     封装为对象        流式传输    过滤/转换    业务模型     状态管理    重建Widget
事件流
Dart 复制代码
用户点击"开始扫描"
    ↓
_checkPermissions() 权限检查
    ↓
_startScanning() 开始扫描
    ↓
FlutterBluePlus.startScan() 原生调用
    ↓
scanResults.listen() 监听结果
    ↓
数据处理 → 添加到_devices
    ↓
setState() → UI更新
关键设计模式
Dart 复制代码
1.观察者模式

// 监听蓝牙状态变化
_bluetoothStateSubscription = FlutterBluePlus.adapterState.listen((state) {
  _updateBluetoothStatus(state);
});

// 监听扫描结果
_scanSubscription = FlutterBluePlus.scanResults.listen((results) {
  // 处理扫描结果
});

2.状态模式

// 根据扫描状态显示不同UI
_isScanning ? "停止扫描" : "开始扫描"
_isScanning ? Colors.red : Colors.blue
_isScanning ? Icons.stop : Icons.play_arrow

3.工厂模式

// BleDeviceInfo 封装扫描结果
BleDeviceInfo deviceInfo = BleDeviceInfo(
  id: device.remoteId.str,
  name: device.platformName,
  rssi: result.rssi,
  // ...
);

4.策略模式

// 根据连接状态显示不同操作
if (_isDeviceConnected(deviceInfo.id)) {
  _showConnectedDeviceOptions(deviceInfo);  // 已连接策略
} else {
  _connectToDevice(deviceInfo);  // 未连接策略
}
组件层级
Dart 复制代码
Scaffold
├── AppBar
├── Column
│   ├── StatusBar (状态显示)
│   ├── ControlButtons (控制按钮)
│   ├── ConnectedDevicesHeader (已连接设备标题)
│   ├── DevicesHeader (设备列表标题)
│   └── Expanded
│       └── ConditionalWidget
│           ├── LoadingWidget
│           ├── EmptyWidget
│           └── RefreshIndicator
│               └── ListView.builder
│                   └── DeviceItem
└── FloatingActionButton (条件显示)
从哪些方面学习这个项目
Dart 复制代码
一、技术栈学习路径

1. Flutter基础(必学)
Dart语言核心:async/await、Future、Stream、空安全、扩展方法
Flutter Widget体系:StatefulWidget、StatelessWidget、生命周期方法
状态管理:setState()、mounted检查、状态提升
UI组件:Material Design组件、布局系统、主题样式
2. 蓝牙开发核心
BLE协议基础:GATT、服务、特征值、UUID
FlutterBluePlus插件:API调用、权限管理、设备发现、连接管理
平台差异:Android和iOS的蓝牙权限和配置
3. 异步编程
Future链式调用:then、catchError、whenComplete
Stream流处理:listen、where、map、transform
并发控制:async/await、Future.wait、StreamSubscription管理

二、项目架构学习要点

1. 分层架构设计
UI层:学习如何将业务逻辑与UI分离
业务逻辑层:权限检查、扫描管理、连接管理
数据层:BleDeviceInfo数据模型、设备列表管理
2. 状态管理模式
setState模式:理解何时调用setState、如何避免过度重建
状态变量设计:isScanning、devices、_connectedDevices的作用
mounted检查:防止dispose后调用setState
3. 生命周期管理
initState:初始化蓝牙状态监听
dispose:清理StreamSubscription和Timer
mounted:异步操作中的组件状态检查

三、核心功能实现学习
1. 权限管理
2. 蓝牙扫描
3. 设备连接管理
4. 数据模型设计

四、UI/UX设计学习

1. 状态反馈
扫描状态:_scanStatus文本、按钮状态、颜色变化
连接状态:已连接设备高亮、连接状态标签
加载状态:CircularProgressIndicator、SnackBar提示
2. 交互设计
列表项设计:ListTile + Card布局
长按操作:onLongPress显示详情
下拉刷新:RefreshIndicator
底部弹窗:showModalBottomSheet
3. 错误处理
权限拒绝:SnackBar提示
连接失败:错误弹窗
蓝牙关闭:状态提示

五、扩展学习方向

1. 进阶功能
服务发现:发现设备的GATT服务
特征值读写:读取/写入设备数据
通知监听:监听特征值变化
OTA升级:固件升级功能
2. 架构升级
状态管理库:Provider、Riverpod、Bloc
路由管理:Navigator 2.0、GoRouter
数据持久化:SharedPreferences、Hive
3. 测试覆盖
单元测试:业务逻辑测试
Widget测试:UI组件测试
集成测试:完整流程测试
相关知识点
Dart 复制代码
1.蓝牙的相关状态

enum BluetoothAdapterState {
  unknown,     // 状态未知
  unavailable, // 不可用(设备不支持)
  unauthorized,// 未授权
  turningOn,   // 正在开启
  on,          // 已开启 
  turningOff,  // 正在关闭
  off,         // 已关闭
}

2.adapterState

FlutterBluePlus.adapterState 是 FlutterBluePlus 插件提供的蓝牙适配器状态流。它是一个 Stream<BluetoothAdapterState>,用于实时监听和获取设备的蓝牙状态变化。

相关方法

listen - 监听状态变化
first - 获取当前/第一个状态
where - 过滤特定状态
distinct - 去重连续相同状态
skip / take - 跳过/获取部分事件
map - 转换状态值
asyncMap / asyncExpand - 异步处理
timeout - 超时控制

3.scanResults

FlutterBluePlus.scanResults 是 蓝牙扫描结果的数据流。当蓝牙扫描启动后,所有发现的设备都会通过这个 Stream 实时推送出来。

优点
实时数据流:持续接收扫描结果
丰富信息:包含设备信息、广播数据、信号强度等
灵活处理:支持各种Stream操作符进行数据处理
跨平台:统一Android和iOS的扫描结果
关键代码
Dart 复制代码
1.获取蓝牙的当前状态
BluetoothAdapterState state = await FlutterBluePlus.adapterState.first;

逻辑
创建对 adapterState Stream 的监听
等待第一个状态数据到来
收到数据(比如 BluetoothAdapterState.on)
取消监听(因为只需要第一个)
返回数据,完成 await

2.Permission.location.request();//获取位置权限
Permission.bluetooth.request();//获取蓝牙权限

判断权限是否给予
Permission.location.isGranted
Permission.bluetooth.isGranted


3.获取已连接的设备
FlutterBluePlus.connectedDevices;

逻辑
Flutter端: 调用 FlutterBluePlus.connectedDevices
平台通道: 通过 MethodChannel 传递到原生平台
Android端: 调用 BluetoothAdapter.getBondedDevices() 或类似API
iOS端: 调用 CBCentralManager.retrieveConnectedPeripherals()
返回: 原生平台返回设备列表给Flutter
转换: FlutterBluePlus将数据转换为 BluetoothDevice 对象列表

4.开始扫描
FlutterBluePlus.startScan(timeout: const Duration(seconds: 15),);

5.获取扫描结果
FlutterBluePlus.scanResults

6.创建设备信息对象
BleDeviceInfo deviceInfo = BleDeviceInfo(
id: device.remoteId.str,
name: device.platformName.isNotEmpty ? device.platformName : null,
localName: result.advertisementData.localName,
rssi: result.rssi,
device: device,
manufacturerData: manufacturerData,
);

7.停止扫描
FlutterBluePlus.stopScan();

8.连接设备
await deviceInfo.device.connect(
  autoConnect: false,// 不自动重连
  timeout: const Duration(seconds: 10),// 10秒超时
      );
9.断开连接
device.disconnect();
实现步骤

1.在pubspec.yaml导入外部类

Dart 复制代码
  flutter_blue_plus: ^1.10.0  # BLE插件
  permission_handler: ^11.0.1

2.在Android的AndroidManifest中添加权限

Dart 复制代码
  <!-- BLE 权限 -->
    <uses-permission android:name="android.permission.BLUETOOTH"/>
    <uses-permission android:name="android.permission.BLUETOOTH_ADMIN"/>

    <!-- Android 6.0+ 需要位置权限扫描BLE -->
    <uses-permission android:name="android.permission.ACCESS_FINE_LOCATION"/>
    <uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION"/>

    <!-- Android 12+ 新增权限 -->
    <uses-permission android:name="android.permission.BLUETOOTH_SCAN"
       />
    <uses-permission android:name="android.permission.BLUETOOTH_CONNECT"/>

    <!-- 声明使用BLE -->
    <uses-feature android:name="android.hardware.bluetooth_le" android:required="true"/>

3.新建一个BLE设备模型,接收扫描到的设备

Dart 复制代码
import 'dart:ui';

import 'package:flutter/cupertino.dart';
import 'package:flutter/material.dart';
import 'package:flutter_blue_plus/flutter_blue_plus.dart';


/// BLE设备模型

class BleDeviceInfo {
  final String id; //设备唯一标识符
  final String? name;
  final String? localName;
  final int rssi;
  final BluetoothDevice device; //核心设备对象
  final int? manufacturerData;
  final String? macAddress;

  BleDeviceInfo({
    required this.id,
    required this.name,
    required this.localName,
    required this.rssi,
    required this.device,
    this.manufacturerData,
    this.macAddress,
  });

  // 获取显示名称
  String get displayName {
    if (localName != null && localName!.isNotEmpty) return localName!;
    if (name != null && name!.isNotEmpty) return name!;
    if (macAddress != null) return '设备 $macAddress';
    return '未知设备';
  }

  // 格式化信号强度
  String get formattedRssi {
    if (rssi == 0) return '未知';
    return '${rssi}dBm';
  }

  Color get signalColor {
    if (rssi >= -50) return Colors.green;
    if (rssi >= -70) return Colors.orange;
    if (rssi >= -90) return Colors.amber;
    return Colors.red;
  }

// 信号强度图标
  IconData get signalIcon {
    if (rssi >= -50) return Icons.network_wifi;      // 满格WiFi
    if (rssi >= -70) return Icons.network_wifi_3_bar; // 3格WiFi
    if (rssi >= -90) return Icons.network_wifi_2_bar; // 2格WiFi
    return Icons.network_wifi_1_bar;                 // 1格WiFi
  }

}

4.新建BLE扫描类,这个类里面包括了UI的实现,先定义一些变量

Dart 复制代码
 // 扫描状态
  bool _isScanning = false;
  bool _isLoading = false;
  String _scanStatus = '准备扫描';

  // 设备列表
  final Map<String, BleDeviceInfo> _devices = {};
  List<BluetoothDevice> _connectedDevices = [];

  // 扫描控制器
  StreamSubscription<List<ScanResult>>? _scanSubscription;
  Timer? _scanTimeoutTimer;
  StreamSubscription<BluetoothAdapterState>? _bluetoothStateSubscription;

5.初始化和销毁

Dart 复制代码
  @override
  void initState() {
    super.initState();
    _initBluetooth();
  }

  @override
  void dispose() {
    _stopScanning();
    _scanSubscription?.cancel();
    _scanTimeoutTimer?.cancel();
    _bluetoothStateSubscription?.cancel();
    super.dispose();
  }

6.初始化蓝牙

Dart 复制代码
Future<void> _initBluetooth() async {
    //先获取当前状态
    try {
      BluetoothAdapterState currentState = await FlutterBluePlus.adapterState.first;
      _updateBluetoothStatus(currentState);
    } catch (e) {
      // 错误处理
      setState(() {
        _scanStatus = '获取蓝牙状态失败';
      });
      return;
    }

    //然后设置监听器(监听未来变化)
    _bluetoothStateSubscription = FlutterBluePlus.adapterState.listen((state) {
      _updateBluetoothStatus(state);
    });
  }

7.获取蓝牙的当前状态

Dart 复制代码
  void _updateBluetoothStatus(BluetoothAdapterState state) {
    setState(() {
      if (state == BluetoothAdapterState.on) {
        _scanStatus = '蓝牙已开启,准备扫描';
      } else {
        _scanStatus = '蓝牙未开启';
        _isScanning = false;
      }
    });

    // 如果蓝牙关闭,停止扫描
    if (state != BluetoothAdapterState.on) {
      _stopScanning();
    }
  }

8.检查并请求权限

Dart 复制代码
Future<bool> _checkPermissions() async {
    // 检查蓝牙状态
    BluetoothAdapterState state = await FlutterBluePlus.adapterState.first;
    if (state != BluetoothAdapterState.on) {
      if (mounted) {
        ScaffoldMessenger.of(context).showSnackBar(
          const SnackBar(
            content: Text('请先开启蓝牙'),
            backgroundColor: Colors.orange,
          ),
        );
      }
      return false;
    }

    // 请求位置权限(Android需要)
    if (!await Permission.location.isGranted) {
      var status = await Permission.location.request();
      if (!status.isGranted) {
        if (mounted) {
          ScaffoldMessenger.of(context).showSnackBar(
            const SnackBar(
              content: Text('需要位置权限来扫描蓝牙设备'),
              backgroundColor: Colors.red,
            ),
          );
        }
        return false;
      }
    }

    // 检查蓝牙权限
    if (!await Permission.bluetooth.isGranted) {
      var status = await Permission.bluetooth.request();
      if (!status.isGranted) {
        if (mounted) {
          ScaffoldMessenger.of(context).showSnackBar(
            const SnackBar(
              content: Text('需要蓝牙权限'),
              backgroundColor: Colors.red,
            ),
          );
        }
        return false;
      }
    }

    return true;
  }

9.开始扫描

Dart 复制代码
Future<void> _startScanning() async {

    //防止重复扫描
    if (_isScanning) return;

    // 检查权限
    if (!await _checkPermissions()) {
      return;
    }

    setState(() {
      _isLoading = true;
      _devices.clear(); // 清空旧设备列表
    });

    try {

      // 获取已连接的设备
      _connectedDevices = await FlutterBluePlus.connectedDevices;

      setState(() {
        _isScanning = true;
        _isLoading = false;
        _scanStatus = '扫描中...';
      });

      // 开始扫描
      await FlutterBluePlus.startScan(timeout: const Duration(seconds: 15),);

      // 设置扫描超时
      _scanTimeoutTimer = Timer(const Duration(seconds: 15), () {
        _stopScanning();
      });

      // 监听扫描结果
      _scanSubscription = FlutterBluePlus.scanResults.listen((results) {

        if (!mounted) return;

        setState(() {
          for (ScanResult result in results) {

            // 获取设备基本信息
            BluetoothDevice device = result.device;

            // 提取制造商数据
            int? manufacturerData;
            if (result.advertisementData.manufacturerData.isNotEmpty) {
              manufacturerData = result.advertisementData.manufacturerData.keys.first;
            }

            // 获取设备名称(优先使用广播名称)
            String? deviceName = result.advertisementData.localName ?? device.platformName;

            // 如果设备没有名字,跳过
            if (deviceName.isEmpty) {
              continue;  // 跳过这个设备
            }

            // 创建设备信息对象
            BleDeviceInfo deviceInfo = BleDeviceInfo(
              id: device.remoteId.str,
              name: device.platformName.isNotEmpty ? device.platformName : null,
              localName: result.advertisementData.localName,
              rssi: result.rssi,
              device: device,
              manufacturerData: manufacturerData,
            );

            // 更新设备列表
            _devices[device.remoteId.str] = deviceInfo;
          }
        });
      }, onError: (error) {
        print('扫描错误: $error');
      });


    } catch (e) {
      print('扫描失败: $e');
      if (mounted) {
        setState(() {
          _isLoading = false;
          _scanStatus = '扫描失败';
        });
        ScaffoldMessenger.of(context).showSnackBar(
          SnackBar(
            content: Text('扫描失败: ${e.toString()}'),
            backgroundColor: Colors.red,
          ),
        );
      }
    }
  }

10.停止扫描

Dart 复制代码
  void _stopScanning() {

    if (!_isScanning) return;

    FlutterBluePlus.stopScan();
    _scanSubscription?.cancel();
    _scanTimeoutTimer?.cancel();

    if (mounted) {
      setState(() {
        _isScanning = false;
        _scanStatus = '扫描完成';
      });
    }
  }

11.清理设备列表

Dart 复制代码
  void _clearDevices() {
    if (mounted) {
      setState(() {
        _devices.clear();
      });
    }
  }

12.检查设备是否已连接

Dart 复制代码
bool _isDeviceConnected(String deviceId) {
    return _connectedDevices.any((device) => device.remoteId.str == deviceId);
  }

13.连接设备

Dart 复制代码
Future<void> _connectToDevice(BleDeviceInfo deviceInfo) async {

    if (_isDeviceConnected(deviceInfo.id)) {
      // 设备已连接,显示选项
      _showConnectedDeviceOptions(deviceInfo);
      return;
    }

    showDialog(
      context: context,
      barrierDismissible: false,//用户不能点击外部关闭
      builder: (context) => AlertDialog(
        title: const Text('连接设备'),
        content: Text('正在连接到 ${deviceInfo.displayName}...'),
      ),
    );

    try {

      //发起连接设备的请求
      await deviceInfo.device.connect(
        autoConnect: false,// 不自动重连
        timeout: const Duration(seconds: 10),// 10秒超时
      );

      if (mounted) {

        Navigator.pop(context);

        // 更新已连接设备列表
        _connectedDevices = await FlutterBluePlus.connectedDevices;

        ScaffoldMessenger.of(context).showSnackBar(
          SnackBar(
            content: Text('连接成功: ${deviceInfo.displayName}'),
            backgroundColor: Colors.green,
            duration: const Duration(seconds: 2),
          ),
        );

        // 刷新UI
        setState(() {});
      }

    } catch (e) {
      if (mounted) {
        Navigator.pop(context);
        ScaffoldMessenger.of(context).showSnackBar(
          SnackBar(
            content: Text('连接失败: ${e.toString()}'),
            backgroundColor: Colors.red,
          ),
        );
      }
    }
  }

14.断开连接

Dart 复制代码
Future<void> _disconnectDevice(BluetoothDevice device) async {

    showDialog(
      context: context,
      barrierDismissible: false,
      builder: (context) => AlertDialog(
        title: const Text('断开连接'),
        content: Text('正在断开与 ${device.platformName} 的连接...'),
      ),
    );

    try {

      await device.disconnect();

      if (mounted) {
        Navigator.pop(context);

        // 更新已连接设备列表
        _connectedDevices = await FlutterBluePlus.connectedDevices;

        ScaffoldMessenger.of(context).showSnackBar(
          const SnackBar(
            content: Text('已断开连接'),
            backgroundColor: Colors.green,
            duration: Duration(seconds: 2),
          ),
        );

        // 刷新UI
        setState(() {});
      }
    } catch (e) {
      if (mounted) {
        Navigator.pop(context);
        ScaffoldMessenger.of(context).showSnackBar(
          SnackBar(
            content: Text('断开连接失败: ${e.toString()}'),
            backgroundColor: Colors.red,
          ),
        );
      }
    }
  }

15.显示已连接设备的设备选项

Dart 复制代码
void _showConnectedDeviceOptions(BleDeviceInfo deviceInfo) {

    showModalBottomSheet(
      context: context,
      builder: (context) => Container(
        padding: const EdgeInsets.all(16),
        child: Column(
          mainAxisSize: MainAxisSize.min,
          children: [

            ListTile(
              leading: const Icon(Icons.bluetooth_connected, color: Colors.green),
              title: Text('${deviceInfo.displayName} 已连接'),
              subtitle: const Text('点击管理连接'),
            ),

            const Divider(),

            ListTile(
              leading: const Icon(Icons.settings),
              title: const Text('设备设置'),
              onTap: () {
                Navigator.pop(context);
                // TODO: 跳转到设备设置页面
              },
            ),
            ListTile(
              leading: const Icon(Icons.link_off, color: Colors.red),
              title: const Text('断开连接'),
              onTap: () {
                Navigator.pop(context);
                _disconnectDevice(deviceInfo.device);
              },
            ),
            const SizedBox(height: 8),
            TextButton(
              onPressed: () => Navigator.pop(context),
              child: const Text('取消'),
            ),
          ],
        ),
      ),
    );
  }

16.构建设备列表项

Dart 复制代码
 Widget _buildDeviceItem(BleDeviceInfo device) {

    bool isConnected = _isDeviceConnected(device.id);

    return Card(
      margin: const EdgeInsets.symmetric(horizontal: 12, vertical: 4),
      elevation: 2,
      color: isConnected ? Colors.green[50] : null,
      child: ListTile(

        //左侧图标
        leading: Container(
          width: 50,
          height: 50,
          decoration: BoxDecoration(
            color: isConnected ? Colors.green[100] : Colors.blue[50],
            borderRadius: BorderRadius.circular(25),
          ),
          child: Icon(
            isConnected ? Icons.bluetooth_connected : Icons.bluetooth,
            color: isConnected ? Colors.green : Colors.blue,
            size: 30,
          ),
        ),

        //标题
        title: Column(
          crossAxisAlignment: CrossAxisAlignment.start,
          children: [
            Row(
              children: [
                Expanded(
                  child: Text(
                    device.displayName,
                    style: TextStyle(
                      fontWeight: FontWeight.bold,
                      fontSize: 16,
                      color: isConnected ? Colors.green[800] : Colors.black,
                    ),
                    maxLines: 1,
                    overflow: TextOverflow.ellipsis,
                  ),
                ),
                if (isConnected)
                  Container(
                    padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 2),
                    decoration: BoxDecoration(
                      color: Colors.green[100],
                      borderRadius: BorderRadius.circular(12),
                    ),
                    child: Text(
                      '已连接',
                      style: TextStyle(
                        fontSize: 10,
                        color: Colors.green[800],
                        fontWeight: FontWeight.bold,
                      ),
                    ),
                  ),
              ],
            ),
            const SizedBox(height: 4),

            //ID
            Text(
              'ID: ${device.id}',
              style: TextStyle(
                fontSize: 11,
                color: Colors.grey[600],
              ),
              maxLines: 1,
              overflow: TextOverflow.ellipsis,
            ),
          ],
        ),

        //副标题
        subtitle: Row(
          children: [
            Icon(
              device.signalIcon,
              size: 16,
              color: device.signalColor,
            ),
            const SizedBox(width: 4),
            Text(
              device.formattedRssi,
              style: TextStyle(
                fontSize: 12,
                color: device.signalColor,
              ),
            ),
            const SizedBox(width: 8),

          ],
        ),

        //右侧操作按钮
        trailing: IconButton(
          icon: Icon(
            isConnected ? Icons.settings : Icons.bluetooth_connected,
            color: isConnected ? Colors.green : Colors.blue,
          ),
          onPressed: () => _connectToDevice(device),
          tooltip: isConnected ? '管理连接' : '连接设备',
        ),
        onTap: () => _connectToDevice(device),
        onLongPress: () {
          _showDeviceDetails(device); //长按显示详细信息
        },
      ),
    );
  }

17.显示设备详细信息

Dart 复制代码
void _showDeviceDetails(BleDeviceInfo device) {
    bool isConnected = _isDeviceConnected(device.id);

    showModalBottomSheet(
      context: context,
      isScrollControlled: true,
      builder: (context) => Container(
        padding: const EdgeInsets.all(16),
        child: Column(
          mainAxisSize: MainAxisSize.min,
          crossAxisAlignment: CrossAxisAlignment.start,
          children: [
            Center(
              child: Container(
                width: 60,
                height: 4,
                decoration: BoxDecoration(
                  color: Colors.grey[300],
                  borderRadius: BorderRadius.circular(2),
                ),
              ),
            ),
            const SizedBox(height: 16),
            Text(
              '设备详情',
              style: const TextStyle(
                fontSize: 20,
                fontWeight: FontWeight.bold,
              ),
            ),
            const SizedBox(height: 16),
            _buildDetailRow('设备名称', device.displayName),
            _buildDetailRow('设备ID', device.id),
            _buildDetailRow('信号强度', device.formattedRssi),
            _buildDetailRow('连接状态', isConnected ? '已连接' : '未连接'),

            const SizedBox(height: 20),
            Row(
              children: [
                Expanded(
                  child: ElevatedButton(
                    onPressed: () {
                      Navigator.pop(context);
                      _connectToDevice(device);
                    },
                    style: ElevatedButton.styleFrom(
                      backgroundColor: isConnected ? Colors.orange : Colors.blue,
                      foregroundColor: Colors.white,
                    ),
                    child: Text(isConnected ? '管理连接' : '连接设备'),
                  ),
                ),
                const SizedBox(width: 12),
                Expanded(
                  child: OutlinedButton(
                    onPressed: () => Navigator.pop(context),
                    child: const Text('关闭'),
                  ),
                ),
              ],
            ),
            SizedBox(height: MediaQuery.of(context).padding.bottom),
          ],
        ),
      ),
    );
  }

18.行UI的子项

Dart 复制代码
Widget _buildDetailRow(String label, String value) {
    return Padding(
      padding: const EdgeInsets.symmetric(vertical: 4),
      child: Row(
        children: [
          SizedBox(
            width: 80,
            child: Text(
              '$label:',
              style: TextStyle(
                fontWeight: FontWeight.w500,
                color: Colors.grey[700],
              ),
            ),
          ),
          Expanded(
            child: Text(
              value,
              style: TextStyle(color: Colors.grey[900]),
            ),
          ),
        ],
      ),
    );
  }

19.具体的UI的架构

Dart 复制代码
 @override
  Widget build(BuildContext context) {
    return Scaffold(

      //1.标题栏
      appBar: AppBar(
        title: const Text('BLE设备扫描器'),
        actions: [
          if (_devices.isNotEmpty)
            IconButton(
              icon: const Icon(Icons.delete_outline),
              onPressed: _clearDevices,
              tooltip: '清空列表',
            ),
        ],
      ),
      body: Column(
        children: [

          // 2.状态栏
          Container(
            padding: const EdgeInsets.all(16),
            color: Colors.grey[50],
            child: Row(
              mainAxisAlignment: MainAxisAlignment.spaceBetween,
              children: [
                Column(
                  crossAxisAlignment: CrossAxisAlignment.start,
                  children: [

                    Text(
                      '扫描状态',
                      style: TextStyle(
                        fontSize: 12,
                        color: Colors.grey[600],
                      ),
                    ),
                    const SizedBox(height: 4),

                    Text(
                      _scanStatus,
                      style: TextStyle(
                        fontSize: 16,
                        fontWeight: FontWeight.bold,
                        color: _isScanning ? Colors.blue : Colors.grey[800],
                      ),
                    ),
                  ],
                ),
                Column(
                  crossAxisAlignment: CrossAxisAlignment.end,
                  children: [

                    Text(
                      '发现设备',
                      style: TextStyle(
                        fontSize: 12,
                        color: Colors.grey[600],
                      ),
                    ),
                    const SizedBox(height: 4),

                    Text(
                      '${_devices.length} 个',
                      style: const TextStyle(
                        fontSize: 16,
                        fontWeight: FontWeight.bold,
                        color: Colors.green,
                      ),
                    ),
                  ],
                ),
              ],
            ),
          ),

          //3. 控制按钮
          Container(
            padding: const EdgeInsets.all(16),
            child: Row(
              children: [
                Expanded(
                  child: ElevatedButton.icon(
                    icon: Icon(
                      _isScanning ? Icons.stop : Icons.play_arrow,
                      size: 24,
                    ),
                    label: Text(
                      _isScanning ? '停止扫描' : '开始扫描',
                      style: const TextStyle(fontSize: 16),
                    ),
                    onPressed: () {
                      if (_isScanning) {
                        _stopScanning();
                      } else {
                        _startScanning();
                      }
                    },
                    style: ElevatedButton.styleFrom(
                      padding: const EdgeInsets.symmetric(vertical: 16),
                      backgroundColor: _isScanning ? Colors.red : Colors.blue,
                      foregroundColor: Colors.white,
                      shape: RoundedRectangleBorder(
                        borderRadius: BorderRadius.circular(12),
                      ),
                    ),
                  ),
                ),
              ],
            ),
          ),

          // 4.显示已连接设备
          if (_connectedDevices.isNotEmpty) ...[
            Padding(
              padding: const EdgeInsets.symmetric(horizontal: 16),
              child: Row(
                children: [
                  const Icon(Icons.bluetooth_connected, color: Colors.green, size: 20),
                  const SizedBox(width: 8),
                  Text(
                    '已连接设备 (${_connectedDevices.length})',
                    style: const TextStyle(
                      fontWeight: FontWeight.bold,
                      color: Colors.green,
                    ),
                  ),
                ],
              ),
            ),
            const SizedBox(height: 8),
          ],

          // 设备列表标题
          Padding(
            padding: const EdgeInsets.symmetric(horizontal: 16),
            child: Row(
              children: [
                Icon(Icons.list, color: Colors.grey[600]),
                const SizedBox(width: 8),
                Text(
                  '发现的设备 (${_devices.length})',
                  style: TextStyle(
                    fontWeight: FontWeight.bold,
                    color: Colors.grey[700],
                  ),
                ),
                const Spacer(),
                if (_isScanning)
                  Row(
                    children: [
                      Container(
                        width: 8,
                        height: 8,
                        decoration: const BoxDecoration(
                          color: Colors.blue,
                          shape: BoxShape.circle,
                        ),
                      ),
                      const SizedBox(width: 4),
                      const Text(
                        '扫描中',
                        style: TextStyle(
                          fontSize: 12,
                          color: Colors.blue,
                        ),
                      ),
                    ],
                  ),
              ],
            ),
          ),

          // 设备列表
          Expanded(
            child: _isLoading
                ? const Center(
                  child: Column(
                    mainAxisAlignment: MainAxisAlignment.center,
                    children: [
                      CircularProgressIndicator(),
                      SizedBox(height: 16),
                      Text('正在检查权限和蓝牙状态...'),
                    ],
                  ),
                )
                : _devices.isEmpty
                ? buildEmpty()
                : RefreshIndicator(
              onRefresh: () async {
                if (!_isScanning) {
                  await _startScanning();
                }
              },
              child: ListView.builder(
                padding: const EdgeInsets.symmetric(vertical: 8),
                itemCount: _devices.length,
                itemBuilder: (context, index) {
                  String deviceId = _devices.keys.elementAt(index);
                  BleDeviceInfo device = _devices[deviceId]!;
                  return _buildDeviceItem(device);
                },
              ),
            ),
          ),
        ],
      ),

      //浮动操作按钮
      floatingActionButton: _isScanning
          ? FloatingActionButton(
        onPressed: _stopScanning,
        backgroundColor: Colors.red,
        foregroundColor: Colors.white,
        child: const Icon(Icons.stop),
        tooltip: '停止扫描',
      )
          : null,
    );
  }

20.当列表为空的UI显示

Dart 复制代码
Widget buildEmpty() {
    return Center(
      child: Column(
        mainAxisAlignment: MainAxisAlignment.center,
        children: [
          Icon(
            Icons.bluetooth_disabled,
            size: 80,
            color: Colors.grey[300],
          ),
          const SizedBox(height: 16),
          Text(
            '没有发现设备',
            style: TextStyle(
              fontSize: 18,
              color: Colors.grey[500],
            ),
          ),
          const SizedBox(height: 8),
          Text(
            _isScanning
                ? '正在搜索附近的蓝牙设备...'
                : '点击"开始扫描"查找设备',
            style: TextStyle(
              color: Colors.grey[400],
            ),
          ),
          if (!_isScanning)
            Padding(
              padding: const EdgeInsets.only(top: 16),
              child: Column(
                children: [
                  ElevatedButton.icon(
                    icon: const Icon(Icons.info_outline),
                    label: const Text('使用说明'),
                    onPressed: () {
                      showDialog(
                        context: context,
                        builder: (context) => AlertDialog(
                          title: const Text('使用说明'),
                          content: const Text(
                            '1. 确保蓝牙已开启\n'
                                '2. 需要位置和蓝牙权限\n'
                                '3. 确保目标设备处于可发现状态\n'
                                '4. 扫描通常需要10-15秒\n'
                                '5. 某些设备可能需要配对',
                          ),
                          actions: [
                            TextButton(
                              onPressed: () => Navigator.pop(context),
                              child: const Text('确定'),
                            ),
                          ],
                        ),
                      );
                    },
                  ),
                  const SizedBox(height: 8),
                  TextButton(
                    onPressed: () {
                      //提示
                      ScaffoldMessenger.of(context).showSnackBar(
                        SnackBar(
                          content: Text('请手动打开系统蓝牙设置'),
                          duration: Duration(seconds: 2),
                        ),
                      );
                    },
                    child: const Text('打开蓝牙设置'),
                  ),
                ],
              ),
            ),
        ],
      ),
    );
  }
代码实例

ble_scanner

Dart 复制代码
import 'dart:async';
import 'package:flutter/material.dart';
import 'package:flutter_blue_plus/flutter_blue_plus.dart';
import 'package:permission_handler/permission_handler.dart';

import 'ble_device_info.dart';



/// BLE扫描类
class BleScanner extends StatefulWidget {
  const BleScanner({super.key});

  @override
  State<BleScanner> createState() => _BleScannerState();

}

class _BleScannerState extends State<BleScanner> {

  // 扫描状态
  bool _isScanning = false;
  bool _isLoading = false;
  String _scanStatus = '准备扫描';

  // 设备列表
  final Map<String, BleDeviceInfo> _devices = {};
  List<BluetoothDevice> _connectedDevices = [];

  // 扫描控制器
  StreamSubscription<List<ScanResult>>? _scanSubscription;
  Timer? _scanTimeoutTimer;
  StreamSubscription<BluetoothAdapterState>? _bluetoothStateSubscription;

  @override
  void initState() {
    super.initState();
    _initBluetooth();
  }

  @override
  void dispose() {
    _stopScanning();
    _scanSubscription?.cancel();
    _scanTimeoutTimer?.cancel();
    _bluetoothStateSubscription?.cancel();
    super.dispose();
  }

  //=============================初始化蓝牙=======================================
  Future<void> _initBluetooth() async {
    //先获取当前状态
    try {
      BluetoothAdapterState currentState = await FlutterBluePlus.adapterState.first;
      _updateBluetoothStatus(currentState);
    } catch (e) {
      // 错误处理
      setState(() {
        _scanStatus = '获取蓝牙状态失败';
      });
      return;
    }

    //然后设置监听器(监听未来变化)
    _bluetoothStateSubscription = FlutterBluePlus.adapterState.listen((state) {
      _updateBluetoothStatus(state);
    });
  }

  //========================获取蓝牙适配器当前状态=================================
  void _updateBluetoothStatus(BluetoothAdapterState state) {
    setState(() {
      if (state == BluetoothAdapterState.on) {
        _scanStatus = '蓝牙已开启,准备扫描';
      } else {
        _scanStatus = '蓝牙未开启';
        _isScanning = false;
      }
    });

    // 如果蓝牙关闭,停止扫描
    if (state != BluetoothAdapterState.on) {
      _stopScanning();
    }
  }

  //===========================检查并请求权限====================================
  Future<bool> _checkPermissions() async {
    // 检查蓝牙状态
    BluetoothAdapterState state = await FlutterBluePlus.adapterState.first;
    if (state != BluetoothAdapterState.on) {
      if (mounted) {
        ScaffoldMessenger.of(context).showSnackBar(
          const SnackBar(
            content: Text('请先开启蓝牙'),
            backgroundColor: Colors.orange,
          ),
        );
      }
      return false;
    }

    // 请求位置权限(Android需要)
    if (!await Permission.location.isGranted) {
      var status = await Permission.location.request();
      if (!status.isGranted) {
        if (mounted) {
          ScaffoldMessenger.of(context).showSnackBar(
            const SnackBar(
              content: Text('需要位置权限来扫描蓝牙设备'),
              backgroundColor: Colors.red,
            ),
          );
        }
        return false;
      }
    }

    // 检查蓝牙权限
    if (!await Permission.bluetooth.isGranted) {
      var status = await Permission.bluetooth.request();
      if (!status.isGranted) {
        if (mounted) {
          ScaffoldMessenger.of(context).showSnackBar(
            const SnackBar(
              content: Text('需要蓝牙权限'),
              backgroundColor: Colors.red,
            ),
          );
        }
        return false;
      }
    }

    return true;
  }

  //===============================开始扫描=========================================
  Future<void> _startScanning() async {

    //防止重复扫描
    if (_isScanning) return;

    // 检查权限
    if (!await _checkPermissions()) {
      return;
    }

    setState(() {
      _isLoading = true;
      _devices.clear(); // 清空旧设备列表
    });

    try {

      // 获取已连接的设备
      _connectedDevices = await FlutterBluePlus.connectedDevices;

      setState(() {
        _isScanning = true;
        _isLoading = false;
        _scanStatus = '扫描中...';
      });

      // 开始扫描
      await FlutterBluePlus.startScan(timeout: const Duration(seconds: 15),);

      // 设置扫描超时
      _scanTimeoutTimer = Timer(const Duration(seconds: 15), () {
        _stopScanning();
      });

      // 监听扫描结果
      _scanSubscription = FlutterBluePlus.scanResults.listen((results) {

        if (!mounted) return;

        setState(() {
          for (ScanResult result in results) {

            // 获取设备基本信息
            BluetoothDevice device = result.device;

            // 提取制造商数据
            int? manufacturerData;
            if (result.advertisementData.manufacturerData.isNotEmpty) {
              manufacturerData = result.advertisementData.manufacturerData.keys.first;
            }

            // 获取设备名称(优先使用广播名称)
            String? deviceName = result.advertisementData.localName ?? device.platformName;

            // 如果设备没有名字,跳过
            if (deviceName.isEmpty) {
              continue;  // 跳过这个设备
            }

            // 创建设备信息对象
            BleDeviceInfo deviceInfo = BleDeviceInfo(
              id: device.remoteId.str,
              name: device.platformName.isNotEmpty ? device.platformName : null,
              localName: result.advertisementData.localName,
              rssi: result.rssi,
              device: device,
              manufacturerData: manufacturerData,
            );

            // 更新设备列表
            _devices[device.remoteId.str] = deviceInfo;
          }
        });
      }, onError: (error) {
        print('扫描错误: $error');
      });


    } catch (e) {
      print('扫描失败: $e');
      if (mounted) {
        setState(() {
          _isLoading = false;
          _scanStatus = '扫描失败';
        });
        ScaffoldMessenger.of(context).showSnackBar(
          SnackBar(
            content: Text('扫描失败: ${e.toString()}'),
            backgroundColor: Colors.red,
          ),
        );
      }
    }
  }

  //=================================停止扫描===================================
  void _stopScanning() {

    if (!_isScanning) return;

    FlutterBluePlus.stopScan();
    _scanSubscription?.cancel();
    _scanTimeoutTimer?.cancel();

    if (mounted) {
      setState(() {
        _isScanning = false;
        _scanStatus = '扫描完成';
      });
    }
  }

  //===============================清空设备列表===================================
  void _clearDevices() {
    if (mounted) {
      setState(() {
        _devices.clear();
      });
    }
  }

  //===========================检查设备是否已连接==================================
  bool _isDeviceConnected(String deviceId) {
    return _connectedDevices.any((device) => device.remoteId.str == deviceId);
  }

  //=============================连接设备======================================
  Future<void> _connectToDevice(BleDeviceInfo deviceInfo) async {

    if (_isDeviceConnected(deviceInfo.id)) {
      // 设备已连接,显示选项
      _showConnectedDeviceOptions(deviceInfo);
      return;
    }

    showDialog(
      context: context,
      barrierDismissible: false,//用户不能点击外部关闭
      builder: (context) => AlertDialog(
        title: const Text('连接设备'),
        content: Text('正在连接到 ${deviceInfo.displayName}...'),
      ),
    );

    try {

      //发起连接设备的请求
      await deviceInfo.device.connect(
        autoConnect: false,// 不自动重连
        timeout: const Duration(seconds: 10),// 10秒超时
      );

      if (mounted) {

        Navigator.pop(context);

        // 更新已连接设备列表
        _connectedDevices = await FlutterBluePlus.connectedDevices;

        ScaffoldMessenger.of(context).showSnackBar(
          SnackBar(
            content: Text('连接成功: ${deviceInfo.displayName}'),
            backgroundColor: Colors.green,
            duration: const Duration(seconds: 2),
          ),
        );

        // 刷新UI
        setState(() {});
      }

    } catch (e) {
      if (mounted) {
        Navigator.pop(context);
        ScaffoldMessenger.of(context).showSnackBar(
          SnackBar(
            content: Text('连接失败: ${e.toString()}'),
            backgroundColor: Colors.red,
          ),
        );
      }
    }
  }

  //===============================断开连接====================================
  Future<void> _disconnectDevice(BluetoothDevice device) async {

    showDialog(
      context: context,
      barrierDismissible: false,
      builder: (context) => AlertDialog(
        title: const Text('断开连接'),
        content: Text('正在断开与 ${device.platformName} 的连接...'),
      ),
    );

    try {

      await device.disconnect();

      if (mounted) {
        Navigator.pop(context);

        // 更新已连接设备列表
        _connectedDevices = await FlutterBluePlus.connectedDevices;

        ScaffoldMessenger.of(context).showSnackBar(
          const SnackBar(
            content: Text('已断开连接'),
            backgroundColor: Colors.green,
            duration: Duration(seconds: 2),
          ),
        );

        // 刷新UI
        setState(() {});
      }
    } catch (e) {
      if (mounted) {
        Navigator.pop(context);
        ScaffoldMessenger.of(context).showSnackBar(
          SnackBar(
            content: Text('断开连接失败: ${e.toString()}'),
            backgroundColor: Colors.red,
          ),
        );
      }
    }
  }

  //===========================已连接设备的设置选项=================================
  void _showConnectedDeviceOptions(BleDeviceInfo deviceInfo) {

    showModalBottomSheet(
      context: context,
      builder: (context) => Container(
        padding: const EdgeInsets.all(16),
        child: Column(
          mainAxisSize: MainAxisSize.min,
          children: [

            ListTile(
              leading: const Icon(Icons.bluetooth_connected, color: Colors.green),
              title: Text('${deviceInfo.displayName} 已连接'),
              subtitle: const Text('点击管理连接'),
            ),

            const Divider(),

            ListTile(
              leading: const Icon(Icons.settings),
              title: const Text('设备设置'),
              onTap: () {
                Navigator.pop(context);
                // TODO: 跳转到设备设置页面
              },
            ),
            ListTile(
              leading: const Icon(Icons.link_off, color: Colors.red),
              title: const Text('断开连接'),
              onTap: () {
                Navigator.pop(context);
                _disconnectDevice(deviceInfo.device);
              },
            ),
            const SizedBox(height: 8),
            TextButton(
              onPressed: () => Navigator.pop(context),
              child: const Text('取消'),
            ),
          ],
        ),
      ),
    );
  }


  //==========================构建设备列表项=======================================
  Widget _buildDeviceItem(BleDeviceInfo device) {

    bool isConnected = _isDeviceConnected(device.id);

    return Card(
      margin: const EdgeInsets.symmetric(horizontal: 12, vertical: 4),
      elevation: 2,
      color: isConnected ? Colors.green[50] : null,
      child: ListTile(

        //左侧图标
        leading: Container(
          width: 50,
          height: 50,
          decoration: BoxDecoration(
            color: isConnected ? Colors.green[100] : Colors.blue[50],
            borderRadius: BorderRadius.circular(25),
          ),
          child: Icon(
            isConnected ? Icons.bluetooth_connected : Icons.bluetooth,
            color: isConnected ? Colors.green : Colors.blue,
            size: 30,
          ),
        ),

        //标题
        title: Column(
          crossAxisAlignment: CrossAxisAlignment.start,
          children: [
            Row(
              children: [
                Expanded(
                  child: Text(
                    device.displayName,
                    style: TextStyle(
                      fontWeight: FontWeight.bold,
                      fontSize: 16,
                      color: isConnected ? Colors.green[800] : Colors.black,
                    ),
                    maxLines: 1,
                    overflow: TextOverflow.ellipsis,
                  ),
                ),
                if (isConnected)
                  Container(
                    padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 2),
                    decoration: BoxDecoration(
                      color: Colors.green[100],
                      borderRadius: BorderRadius.circular(12),
                    ),
                    child: Text(
                      '已连接',
                      style: TextStyle(
                        fontSize: 10,
                        color: Colors.green[800],
                        fontWeight: FontWeight.bold,
                      ),
                    ),
                  ),
              ],
            ),
            const SizedBox(height: 4),

            //ID
            Text(
              'ID: ${device.id}',
              style: TextStyle(
                fontSize: 11,
                color: Colors.grey[600],
              ),
              maxLines: 1,
              overflow: TextOverflow.ellipsis,
            ),
          ],
        ),

        //副标题
        subtitle: Row(
          children: [
            Icon(
              device.signalIcon,
              size: 16,
              color: device.signalColor,
            ),
            const SizedBox(width: 4),
            Text(
              device.formattedRssi,
              style: TextStyle(
                fontSize: 12,
                color: device.signalColor,
              ),
            ),
            const SizedBox(width: 8),

          ],
        ),

        //右侧操作按钮
        trailing: IconButton(
          icon: Icon(
            isConnected ? Icons.settings : Icons.bluetooth_connected,
            color: isConnected ? Colors.green : Colors.blue,
          ),
          onPressed: () => _connectToDevice(device),
          tooltip: isConnected ? '管理连接' : '连接设备',
        ),
        onTap: () => _connectToDevice(device),
        onLongPress: () {
          _showDeviceDetails(device); //长按显示详细信息
        },
      ),
    );
  }

  //=============================显示设备详细信息==================================
  void _showDeviceDetails(BleDeviceInfo device) {
    bool isConnected = _isDeviceConnected(device.id);

    showModalBottomSheet(
      context: context,
      isScrollControlled: true,
      builder: (context) => Container(
        padding: const EdgeInsets.all(16),
        child: Column(
          mainAxisSize: MainAxisSize.min,
          crossAxisAlignment: CrossAxisAlignment.start,
          children: [
            Center(
              child: Container(
                width: 60,
                height: 4,
                decoration: BoxDecoration(
                  color: Colors.grey[300],
                  borderRadius: BorderRadius.circular(2),
                ),
              ),
            ),
            const SizedBox(height: 16),
            Text(
              '设备详情',
              style: const TextStyle(
                fontSize: 20,
                fontWeight: FontWeight.bold,
              ),
            ),
            const SizedBox(height: 16),
            _buildDetailRow('设备名称', device.displayName),
            _buildDetailRow('设备ID', device.id),
            _buildDetailRow('信号强度', device.formattedRssi),
            _buildDetailRow('连接状态', isConnected ? '已连接' : '未连接'),

            const SizedBox(height: 20),
            Row(
              children: [
                Expanded(
                  child: ElevatedButton(
                    onPressed: () {
                      Navigator.pop(context);
                      _connectToDevice(device);
                    },
                    style: ElevatedButton.styleFrom(
                      backgroundColor: isConnected ? Colors.orange : Colors.blue,
                      foregroundColor: Colors.white,
                    ),
                    child: Text(isConnected ? '管理连接' : '连接设备'),
                  ),
                ),
                const SizedBox(width: 12),
                Expanded(
                  child: OutlinedButton(
                    onPressed: () => Navigator.pop(context),
                    child: const Text('关闭'),
                  ),
                ),
              ],
            ),
            SizedBox(height: MediaQuery.of(context).padding.bottom),
          ],
        ),
      ),
    );
  }


  //======================行UI的子项============================================
  Widget _buildDetailRow(String label, String value) {
    return Padding(
      padding: const EdgeInsets.symmetric(vertical: 4),
      child: Row(
        children: [
          SizedBox(
            width: 80,
            child: Text(
              '$label:',
              style: TextStyle(
                fontWeight: FontWeight.w500,
                color: Colors.grey[700],
              ),
            ),
          ),
          Expanded(
            child: Text(
              value,
              style: TextStyle(color: Colors.grey[900]),
            ),
          ),
        ],
      ),
    );
  }


  @override
  Widget build(BuildContext context) {
    return Scaffold(

      //1.标题栏
      appBar: AppBar(
        title: const Text('BLE设备扫描器'),
        actions: [
          if (_devices.isNotEmpty)
            IconButton(
              icon: const Icon(Icons.delete_outline),
              onPressed: _clearDevices,
              tooltip: '清空列表',
            ),
        ],
      ),
      body: Column(
        children: [

          // 2.状态栏
          Container(
            padding: const EdgeInsets.all(16),
            color: Colors.grey[50],
            child: Row(
              mainAxisAlignment: MainAxisAlignment.spaceBetween,
              children: [
                Column(
                  crossAxisAlignment: CrossAxisAlignment.start,
                  children: [

                    Text(
                      '扫描状态',
                      style: TextStyle(
                        fontSize: 12,
                        color: Colors.grey[600],
                      ),
                    ),
                    const SizedBox(height: 4),

                    Text(
                      _scanStatus,
                      style: TextStyle(
                        fontSize: 16,
                        fontWeight: FontWeight.bold,
                        color: _isScanning ? Colors.blue : Colors.grey[800],
                      ),
                    ),
                  ],
                ),
                Column(
                  crossAxisAlignment: CrossAxisAlignment.end,
                  children: [

                    Text(
                      '发现设备',
                      style: TextStyle(
                        fontSize: 12,
                        color: Colors.grey[600],
                      ),
                    ),
                    const SizedBox(height: 4),

                    Text(
                      '${_devices.length} 个',
                      style: const TextStyle(
                        fontSize: 16,
                        fontWeight: FontWeight.bold,
                        color: Colors.green,
                      ),
                    ),
                  ],
                ),
              ],
            ),
          ),

          //3. 控制按钮
          Container(
            padding: const EdgeInsets.all(16),
            child: Row(
              children: [
                Expanded(
                  child: ElevatedButton.icon(
                    icon: Icon(
                      _isScanning ? Icons.stop : Icons.play_arrow,
                      size: 24,
                    ),
                    label: Text(
                      _isScanning ? '停止扫描' : '开始扫描',
                      style: const TextStyle(fontSize: 16),
                    ),
                    onPressed: () {
                      if (_isScanning) {
                        _stopScanning();
                      } else {
                        _startScanning();
                      }
                    },
                    style: ElevatedButton.styleFrom(
                      padding: const EdgeInsets.symmetric(vertical: 16),
                      backgroundColor: _isScanning ? Colors.red : Colors.blue,
                      foregroundColor: Colors.white,
                      shape: RoundedRectangleBorder(
                        borderRadius: BorderRadius.circular(12),
                      ),
                    ),
                  ),
                ),
              ],
            ),
          ),

          // 4.显示已连接设备
          if (_connectedDevices.isNotEmpty) ...[
            Padding(
              padding: const EdgeInsets.symmetric(horizontal: 16),
              child: Row(
                children: [
                  const Icon(Icons.bluetooth_connected, color: Colors.green, size: 20),
                  const SizedBox(width: 8),
                  Text(
                    '已连接设备 (${_connectedDevices.length})',
                    style: const TextStyle(
                      fontWeight: FontWeight.bold,
                      color: Colors.green,
                    ),
                  ),
                ],
              ),
            ),
            const SizedBox(height: 8),
          ],

          // 设备列表标题
          Padding(
            padding: const EdgeInsets.symmetric(horizontal: 16),
            child: Row(
              children: [
                Icon(Icons.list, color: Colors.grey[600]),
                const SizedBox(width: 8),
                Text(
                  '发现的设备 (${_devices.length})',
                  style: TextStyle(
                    fontWeight: FontWeight.bold,
                    color: Colors.grey[700],
                  ),
                ),
                const Spacer(),
                if (_isScanning)
                  Row(
                    children: [
                      Container(
                        width: 8,
                        height: 8,
                        decoration: const BoxDecoration(
                          color: Colors.blue,
                          shape: BoxShape.circle,
                        ),
                      ),
                      const SizedBox(width: 4),
                      const Text(
                        '扫描中',
                        style: TextStyle(
                          fontSize: 12,
                          color: Colors.blue,
                        ),
                      ),
                    ],
                  ),
              ],
            ),
          ),

          // 设备列表
          Expanded(
            child: _isLoading
                ? const Center(
                  child: Column(
                    mainAxisAlignment: MainAxisAlignment.center,
                    children: [
                      CircularProgressIndicator(),
                      SizedBox(height: 16),
                      Text('正在检查权限和蓝牙状态...'),
                    ],
                  ),
                )
                : _devices.isEmpty
                ? buildEmpty()
                : RefreshIndicator(
              onRefresh: () async {
                if (!_isScanning) {
                  await _startScanning();
                }
              },
              child: ListView.builder(
                padding: const EdgeInsets.symmetric(vertical: 8),
                itemCount: _devices.length,
                itemBuilder: (context, index) {
                  String deviceId = _devices.keys.elementAt(index);
                  BleDeviceInfo device = _devices[deviceId]!;
                  return _buildDeviceItem(device);
                },
              ),
            ),
          ),
        ],
      ),

      //浮动操作按钮
      floatingActionButton: _isScanning
          ? FloatingActionButton(
        onPressed: _stopScanning,
        backgroundColor: Colors.red,
        foregroundColor: Colors.white,
        child: const Icon(Icons.stop),
        tooltip: '停止扫描',
      )
          : null,
    );
  }


  //==================================空列表==================================
  Widget buildEmpty() {
    return Center(
      child: Column(
        mainAxisAlignment: MainAxisAlignment.center,
        children: [
          Icon(
            Icons.bluetooth_disabled,
            size: 80,
            color: Colors.grey[300],
          ),
          const SizedBox(height: 16),
          Text(
            '没有发现设备',
            style: TextStyle(
              fontSize: 18,
              color: Colors.grey[500],
            ),
          ),
          const SizedBox(height: 8),
          Text(
            _isScanning
                ? '正在搜索附近的蓝牙设备...'
                : '点击"开始扫描"查找设备',
            style: TextStyle(
              color: Colors.grey[400],
            ),
          ),
          if (!_isScanning)
            Padding(
              padding: const EdgeInsets.only(top: 16),
              child: Column(
                children: [
                  ElevatedButton.icon(
                    icon: const Icon(Icons.info_outline),
                    label: const Text('使用说明'),
                    onPressed: () {
                      showDialog(
                        context: context,
                        builder: (context) => AlertDialog(
                          title: const Text('使用说明'),
                          content: const Text(
                            '1. 确保蓝牙已开启\n'
                                '2. 需要位置和蓝牙权限\n'
                                '3. 确保目标设备处于可发现状态\n'
                                '4. 扫描通常需要10-15秒\n'
                                '5. 某些设备可能需要配对',
                          ),
                          actions: [
                            TextButton(
                              onPressed: () => Navigator.pop(context),
                              child: const Text('确定'),
                            ),
                          ],
                        ),
                      );
                    },
                  ),
                  const SizedBox(height: 8),
                  TextButton(
                    onPressed: () {
                      //提示
                      ScaffoldMessenger.of(context).showSnackBar(
                        SnackBar(
                          content: Text('请手动打开系统蓝牙设置'),
                          duration: Duration(seconds: 2),
                        ),
                      );
                    },
                    child: const Text('打开蓝牙设置'),
                  ),
                ],
              ),
            ),
        ],
      ),
    );
  }
}
相关推荐
2601_949575862 小时前
JSON5 跨端三方库仓颉适配全流程
前端·javascript·flutter
法的空间2 小时前
AI 编程时代,为什么 Dart 分析会拖慢大型项目的开发
android·flutter·ios
恋猫de小郭4 小时前
Flutter 紧急发布 3.47.8 安全升级版本,建议都升级
android·前端·flutter
好的佩奇20 小时前
Dart 之模式
android·flutter·dart
淡淡的香烟1 天前
Flutter实现多语言切换
android·flutter·1024程序员节
事圆则缓1 天前
从零搭建一套 Flutter 架构:先跑通数据流
flutter
程序员老刘2 天前
Flutter版本选择指南:3.44稳定了11周正式转正 | 2026年10月
flutter·ai编程·客户端
chicken_h2 天前
【无标题】
flutter·打包
事圆则缓2 天前
Flutter 底层渲染引擎怎么变了:从 Skia 到 Impeller,以及迁移时的坑
flutter
码林鼠3 天前
flutter状态管理入门教程-Riverpod
开发语言·javascript·flutter