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('打开蓝牙设置'),
                  ),
                ],
              ),
            ),
        ],
      ),
    );
  }
}
相关推荐
消失的旧时光-19432 小时前
Android + Flutter 混合架构全景图:从接入到系统的完整方法论
android·flutter
南村群童欺我老无力.2 小时前
Flutter 框架跨平台鸿蒙开发-鸿蒙计算器开发教程
vscode·flutter·华为·typescript·harmonyos
LawrenceLan2 小时前
16.Flutter 零基础入门(十六):Widget 基础概念与第一个 Flutter 页面
开发语言·前端·flutter·dart
南村群童欺我老无力.2 小时前
Flutter 框架跨平台鸿蒙开发 - 从零开发经典推箱子游戏
flutter·游戏·华为·typescript·harmonyos
LawrenceLan2 小时前
17.Flutter 零基础入门(十七):StatelessWidget 与 State 的第一次分离
开发语言·前端·flutter·dart
柒儿吖2 小时前
Flutter跨平台三方库image_picker在鸿蒙中的使用指南
flutter·华为·harmonyos
世人万千丶2 小时前
鸿蒙跨端框架Flutter学习day 2、常用UI组件-折行布局 Wrap & Chip
学习·flutter·ui·华为·harmonyos·鸿蒙
柒儿吖2 小时前
Flutter跨平台三方库file_selector在鸿蒙中的使用指南
flutter·华为·harmonyos
柒儿吖3 小时前
Flutter跨平台三方库url_launcher在鸿蒙中的使用指南
flutter·华为·harmonyos