随着物联网(IoT)技术的普及,移动端与智能硬件的交互成为高频开发场景。Flutter 凭借跨平台优势和丰富的原生插件生态,能快速实现蓝牙通信实时数据监控设备指令下发等核心功能。本文将以智能温湿度监测设备为场景,开发一套支持蓝牙连接、数据实时刷新、设备远程控制的 Flutter 应用,覆盖 IoT 开发的关键技术点。

一、技术栈与环境准备

1. 核心依赖

IoT 开发的核心是蓝牙通信数据解析,需添加以下依赖到 pubspec.yaml

dependencies:
  flutter:
    sdk: flutter
  # 蓝牙低功耗(BLE)通信
  flutter_blue_plus: ^1.13.3
  # 状态管理
  provider: ^6.1.1
  # 图表展示(温湿度曲线)
  fl_chart: ^0.65.0
  # 权限申请
  permission_handler: ^10.4.0
  # 数据持久化(设备连接记录)
  shared_preferences: ^2.2.2

dev_dependencies:
  flutter_test:
    sdk: flutter

执行 flutter pub get 安装依赖。

2. 权限配置

蓝牙通信需要系统权限,需在原生配置文件中声明:

  • Android:修改 android/app/src/main/AndroidManifest.xml
    <!-- 蓝牙权限 -->
    <uses-permission android:name="android.permission.BLUETOOTH" />
    <uses-permission android:name="android.permission.BLUETOOTH_ADMIN" />
    <uses-permission android:name="android.permission.BLUETOOTH_SCAN" android:usesPermissionFlags="neverForLocation" />
    <uses-permission android:name="android.permission.BLUETOOTH_CONNECT" />
    <uses-feature android:name="android.hardware.bluetooth_le" android:required="true" />
    
  • iOS:修改 ios/Runner/Info.plist
    <key>NSBluetoothAlwaysUsageDescription</key>
    <string>需要蓝牙权限连接智能设备</string>
    <key>NSBluetoothPeripheralUsageDescription</key>
    <string>需要蓝牙权限与设备通信</string>
    

二、核心功能拆解

本应用实现智能温湿度设备的全流程交互:

  1. 蓝牙权限申请:检测并申请蓝牙、位置(Android 需位置权限扫描设备)权限;
  2. 设备扫描与连接:扫描周边 BLE 设备,显示设备列表并支持连接;
  3. 实时数据监控:监听设备温湿度数据,实时更新 UI 与趋势图表;
  4. 远程指令下发:向设备发送控制指令(如开启 / 关闭报警阈值);
  5. 连接记录保存:保存最近连接的设备,下次启动快速重连。

三、完整代码实现

1. 数据模型与状态管理

1.1 设备数据模型(lib/models/device_model.dart)
// 蓝牙设备模型
class BleDevice {
  final String id; // 设备唯一标识
  final String name; // 设备名称
  final int rssi; // 信号强度

  const BleDevice({
    required this.id,
    required this.name,
    required this.rssi,
  });
}

// 温湿度数据模型
class EnvData {
  final double temperature; // 温度(℃)
  final double humidity; // 湿度(%RH)
  final DateTime timestamp; // 采集时间

  const EnvData({
    required this.temperature,
    required this.humidity,
    required this.timestamp,
  });
}
1.2 蓝牙通信状态管理(lib/providers/ble_provider.dart)
import 'package:flutter/foundation.dart';
import 'package:flutter_blue_plus/flutter_blue_plus.dart';
import 'package:shared_preferences/shared_preferences.dart';
import '../models/device_model.dart';

class BleProvider with ChangeNotifier {
  // 蓝牙适配器状态
  BluetoothState _bluetoothState = BluetoothState.unknown;
  BluetoothState get bluetoothState => _bluetoothState;

  // 扫描到的设备列表
  List<BleDevice> _scannedDevices = [];
  List<BleDevice> get scannedDevices => _scannedDevices;

  // 当前连接的设备
  BluetoothDevice? _connectedDevice;
  BluetoothDevice? get connectedDevice => _connectedDevice;

  // 温湿度数据列表(用于绘制图表)
  List<EnvData> _envDataList = [];
  List<EnvData> get envDataList => _envDataList;

  // 最近连接的设备ID
  String? _lastConnectedDeviceId;
  String? get lastConnectedDeviceId => _lastConnectedDeviceId;

  // BLE 服务与特征 UUID(需与硬件端一致)
  static const String SERVICE_UUID = "0000ffb0-0000-1000-8000-00805f9b34fb";
  static const String ENV_CHAR_UUID = "0000ffb2-0000-1000-8000-00805f9b34fb";
  static const String CMD_CHAR_UUID = "0000ffb1-0000-1000-8000-00805f9b34fb";

  BleProvider() {
    // 监听蓝牙状态变化
    FlutterBluePlus.instance.state.listen((state) {
      _bluetoothState = state;
      notifyListeners();
    });

    // 加载最近连接的设备
    _loadLastConnectedDevice();
  }

  // 加载最近连接的设备ID
  Future<void> _loadLastConnectedDevice() async {
    final prefs = await SharedPreferences.getInstance();
    _lastConnectedDeviceId = prefs.getString('last_connected_device_id');
    notifyListeners();
  }

  // 保存最近连接的设备ID
  Future<void> _saveLastConnectedDevice(String deviceId) async {
    final prefs = await SharedPreferences.getInstance();
    await prefs.setString('last_connected_device_id', deviceId);
    _lastConnectedDeviceId = deviceId;
  }

  // 开始扫描设备
  Future<void> startScan() async {
    if (_bluetoothState != BluetoothState.on) {
      // 蓝牙未开启,请求开启
      await FlutterBluePlus.instance.turnOn();
      return;
    }

    _scannedDevices.clear();
    notifyListeners();

    // 扫描周边 BLE 设备,持续 4 秒
    FlutterBluePlus.instance.startScan(timeout: const Duration(seconds: 4));

    // 监听扫描结果
    FlutterBluePlus.instance.scanResults.listen((results) {
      for (ScanResult result in results) {
        final device = BleDevice(
          id: result.device.id.str,
          name: result.device.name.isNotEmpty ? result.device.name : "未知设备",
          rssi: result.rssi,
        );
        // 避免重复添加设备
        if (!_scannedDevices.any((d) => d.id == device.id)) {
          _scannedDevices.add(device);
        }
      }
      notifyListeners();
    });

    // 扫描结束后停止监听
    FlutterBluePlus.instance.isScanning.listen((isScanning) {
      if (!isScanning) {
        notifyListeners();
      }
    });
  }

  // 停止扫描设备
  Future<void> stopScan() async {
    await FlutterBluePlus.instance.stopScan();
  }

  // 连接设备
  Future<void> connectDevice(String deviceId) async {
    final device = BluetoothDevice.fromId(deviceId);
    if (_connectedDevice != null) {
      await disconnectDevice();
    }

    try {
      await device.connect();
      _connectedDevice = device;
      await _saveLastConnectedDevice(deviceId);
      // 连接成功后监听数据
      _listenEnvData();
      notifyListeners();
    } catch (e) {
      if (kDebugMode) {
        print("连接失败: $e");
      }
      rethrow;
    }
  }

  // 断开设备连接
  Future<void> disconnectDevice() async {
    if (_connectedDevice != null) {
      await _connectedDevice!.disconnect();
      _connectedDevice = null;
      _envDataList.clear();
      notifyListeners();
    }
  }

  // 监听温湿度数据
  Future<void> _listenEnvData() async {
    if (_connectedDevice == null) return;

    try {
      // 发现设备服务
      List<BluetoothService> services = await _connectedDevice!.discoverServices();
      final targetService = services.firstWhere(
        (s) => s.uuid.str == SERVICE_UUID,
      );

      // 获取温湿度特征
      final envChar = targetService.characteristics.firstWhere(
        (c) => c.uuid.str == ENV_CHAR_UUID,
      );

      // 启用特征通知
      await envChar.setNotifyValue(true);

      // 监听特征数据变化
      envChar.value.listen((value) {
        // 解析二进制数据(需与硬件端协议一致)
        // 协议示例:value[0] = 温度整数部分, value[1] = 温度小数部分, value[2] = 湿度整数部分, value[3] = 湿度小数部分
        double temp = value[0] + value[1] / 100;
        double humi = value[2] + value[3] / 100;
        final envData = EnvData(
          temperature: temp,
          humidity: humi,
          timestamp: DateTime.now(),
        );

        // 最多保存20条数据,用于绘制趋势图
        if (_envDataList.length >= 20) {
          _envDataList.removeAt(0);
        }
        _envDataList.add(envData);
        notifyListeners();
      });
    } catch (e) {
      if (kDebugMode) {
        print("数据监听失败: $e");
      }
    }
  }

  // 发送控制指令到设备
  Future<void> sendCommand(List<int> command) async {
    if (_connectedDevice == null) return;

    try {
      List<BluetoothService> services = await _connectedDevice!.discoverServices();
      final targetService = services.firstWhere(
        (s) => s.uuid.str == SERVICE_UUID,
      );

      final cmdChar = targetService.characteristics.firstWhere(
        (c) => c.uuid.str == CMD_CHAR_UUID,
      );

      await cmdChar.write(command);
    } catch (e) {
      if (kDebugMode) {
        print("指令发送失败: $e");
      }
      rethrow;
    }
  }
}

2. 权限申请工具类(lib/utils/permission_util.dart)

import 'package:permission_handler/permission_handler.dart';

class PermissionUtil {
  // 申请蓝牙相关权限
  static Future<bool> requestBlePermissions() async {
    Map<Permission, PermissionStatus> statuses = await [
      Permission.bluetooth,
      Permission.bluetoothScan,
      Permission.bluetoothConnect,
      // Android 12 以下需要位置权限扫描BLE设备
      Permission.location,
    ].request();

    // 检查所有权限是否授予
    return statuses.values.every((status) => status.isGranted);
  }
}

3. 核心页面开发

3.1 设备扫描页面(lib/pages/device_scan_page.dart)
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
import '../providers/ble_provider.dart';
import '../utils/permission_util.dart';
import 'device_control_page.dart';

class DeviceScanPage extends StatefulWidget {
  const DeviceScanPage({super.key});

  @override
  State<DeviceScanPage> createState() => _DeviceScanPageState();
}

class _DeviceScanPageState extends State<DeviceScanPage> {
  @override
  void initState() {
    super.initState();
    // 页面加载时申请权限
    _requestPermissions();
  }

  // 申请蓝牙权限
  Future<void> _requestPermissions() async {
    final granted = await PermissionUtil.requestBlePermissions();
    if (granted) {
      // 权限授予后自动扫描设备
      Provider.of<BleProvider>(context, listen: false).startScan();
    } else {
      if (mounted) {
        ScaffoldMessenger.of(context).showSnackBar(
          const SnackBar(content: Text("蓝牙权限未授予,无法扫描设备")),
        );
      }
    }
  }

  // 连接设备并跳转到控制页面
  void _connectAndNavigate(String deviceId) async {
    final bleProvider = Provider.of<BleProvider>(context, listen: false);
    try {
      await bleProvider.connectDevice(deviceId);
      if (mounted) {
        Navigator.pushReplacement(
          context,
          MaterialPageRoute(builder: (context) => const DeviceControlPage()),
        );
      }
    } catch (e) {
      ScaffoldMessenger.of(context).showSnackBar(
        SnackBar(content: Text("连接失败: $e")),
      );
    }
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: const Text("智能设备扫描"),
        actions: [
          IconButton(
            icon: const Icon(Icons.refresh),
            onPressed: () => Provider.of<BleProvider>(context, listen: false).startScan(),
          ),
        ],
      ),
      body: Consumer<BleProvider>(
        builder: (context, provider, child) {
          // 蓝牙未开启
          if (provider.bluetoothState != BluetoothState.on) {
            return Center(
              child: Column(
                mainAxisAlignment: MainAxisAlignment.center,
                children: [
                  const Text("蓝牙未开启"),
                  const SizedBox(height: 20),
                  ElevatedButton(
                    onPressed: () => FlutterBluePlus.instance.turnOn(),
                    child: const Text("开启蓝牙"),
                  ),
                ],
              ),
            );
          }

          // 显示扫描到的设备列表
          return ListView.separated(
            padding: const EdgeInsets.all(16),
            itemCount: provider.scannedDevices.length,
            separatorBuilder: (context, index) => const Divider(),
            itemBuilder: (context, index) {
              final device = provider.scannedDevices[index];
              return ListTile(
                leading: const Icon(Icons.bluetooth, color: Colors.blue),
                title: Text(device.name),
                subtitle: Column(
                  crossAxisAlignment: CrossAxisAlignment.start,
                  children: [
                    Text("设备ID: ${device.id}"),
                    Text("信号强度: ${device.rssi} dBm"),
                  ],
                ),
                trailing: device.id == provider.lastConnectedDeviceId
                    ? const Chip(label: Text("最近连接"))
                    : null,
                onTap: () => _connectAndNavigate(device.id),
              );
            },
          );
        },
      ),
    );
  }
}
3.2 设备控制页面(lib/pages/device_control_page.dart)
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
import 'package:fl_chart/fl_chart.dart';
import '../providers/ble_provider.dart';

class DeviceControlPage extends StatefulWidget {
  const DeviceControlPage({super.key});

  @override
  State<DeviceControlPage> createState() => _DeviceControlPageState();
}

class _DeviceControlPageState extends State<DeviceControlPage> {
  // 报警阈值开关状态
  bool _alarmEnabled = false;

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: const Text("设备控制中心"),
        actions: [
          IconButton(
            icon: const Icon(Icons.bluetooth_disabled),
            onPressed: () {
              Provider.of<BleProvider>(context, listen: false).disconnectDevice();
              Navigator.pop(context);
            },
          ),
        ],
      ),
      body: Consumer<BleProvider>(
        builder: (context, provider, child) {
          // 无数据时显示加载
          if (provider.envDataList.isEmpty) {
            return const Center(child: CircularProgressIndicator());
          }

          // 获取最新数据
          final latestData = provider.envDataList.last;

          return Padding(
            padding: const EdgeInsets.all(16),
            child: Column(
              children: [
                // 温湿度实时显示卡片
                Card(
                  elevation: 4,
                  child: Padding(
                    padding: const EdgeInsets.all(20),
                    child: Row(
                      mainAxisAlignment: MainAxisAlignment.spaceAround,
                      children: [
                        Column(
                          children: [
                            const Text("温度", style: TextStyle(fontSize: 18)),
                            const SizedBox(height: 10),
                            Text(
                              "${latestData.temperature.toStringAsFixed(1)} ℃",
                              style: const TextStyle(
                                fontSize: 32,
                                fontWeight: FontWeight.bold,
                                color: Colors.orange,
                              ),
                            ),
                          ],
                        ),
                        Column(
                          children: [
                            const Text("湿度", style: TextStyle(fontSize: 18)),
                            const SizedBox(height: 10),
                            Text(
                              "${latestData.humidity.toStringAsFixed(1)} %RH",
                              style: const TextStyle(
                                fontSize: 32,
                                fontWeight: FontWeight.bold,
                                color: Colors.blue,
                              ),
                            ),
                          ],
                        ),
                      ],
                    ),
                  ),
                ),
                const SizedBox(height: 20),
                // 报警阈值开关
                SwitchListTile(
                  title: const Text("开启温湿度报警阈值"),
                  value: _alarmEnabled,
                  onChanged: (value) async {
                    setState(() {
                      _alarmEnabled = value;
                    });
                    // 发送指令到设备(0x00=关闭,0x01=开启)
                    await provider.sendCommand([value ? 0x01 : 0x00]);
                  },
                ),
                const SizedBox(height: 20),
                // 温湿度趋势图表
                const Text(
                  "温湿度趋势(近20条数据)",
                  style: TextStyle(fontSize: 16, fontWeight: FontWeight.bold),
                ),
                const SizedBox(height: 10),
                Expanded(
                  child: LineChart(
                    LineChartData(
                      gridData: const FlGridData(show: true),
                      titlesData: const FlTitlesData(
                        bottomTitles: AxisTitles(
                          sideTitles: SideTitles(showTitles: false),
                        ),
                        leftTitles: AxisTitles(
                          sideTitles: SideTitles(showTitles: true),
                        ),
                      ),
                      borderData: FlBorderData(show: true),
                      lineBarsData: [
                        // 温度曲线
                        LineChartBarData(
                          spots: provider.envDataList.asMap().entries.map((e) {
                            return FlSpot(e.key.toDouble(), e.value.temperature);
                          }).toList(),
                          color: Colors.orange,
                          isCurved: true,
                          dotData: const FlDotData(show: false),
                          label: const LineChartBarLabel(label: "温度"),
                        ),
                        // 湿度曲线
                        LineChartBarData(
                          spots: provider.envDataList.asMap().entries.map((e) {
                            return FlSpot(e.key.toDouble(), e.value.humidity);
                          }).toList(),
                          color: Colors.blue,
                          isCurved: true,
                          dotData: const FlDotData(show: false),
                          label: const LineChartBarLabel(label: "湿度"),
                        ),
                      ],
                    ),
                  ),
                ),
              ],
            ),
          );
        },
      ),
    );
  }
}

4. 应用入口(lib/main.dart)

import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
import 'package:flutter_blue_plus/flutter_blue_plus.dart';
import 'providers/ble_provider.dart';
import 'pages/device_scan_page.dart';

void main() {
  runApp(
    MultiProvider(
      providers: [
        ChangeNotifierProvider(create: (context) => BleProvider()),
      ],
      child: const MyApp(),
    ),
  );
}

class MyApp extends StatelessWidget {
  const MyApp({super.key});

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Flutter IoT 设备控制',
      theme: ThemeData(
        primarySwatch: Colors.blue,
        useMaterial3: true,
      ),
      home: const DeviceScanPage(),
      debugShowCheckedModeBanner: false,
    );
  }
}

四、核心能力解析

1. BLE 蓝牙通信

  • flutter_blue_plus 插件:目前 Flutter 生态中最稳定的 BLE 通信插件,支持设备扫描、连接、特征读写、通知监听等全功能;
  • UUID 匹配:硬件端与软件端需约定统一的服务 UUID 和特征 UUID,才能正确读写数据;
  • 数据解析:硬件端通常以二进制格式传输数据,软件端需按照约定的协议解析(如本文中 4 字节分别存储温湿度的整数和小数部分)。

2. 状态管理与数据共享

  • Provider 全局状态:将蓝牙连接状态、设备列表、温湿度数据等全局状态放入 BleProvider,实现跨页面数据共享;
  • 实时数据监听:通过 characteristic.value.listen 监听硬件数据变化,实时更新 UI 和图表;
  • 连接记录持久化:使用 shared_preferences 保存最近连接的设备 ID,下次启动可快速重连。

3. 可视化数据展示

  • fl_chart 图表绘制:将温湿度数据绘制成折线图,直观展示数据趋势;
  • 卡片化 UI 设计:温湿度数据使用 Card 组件包裹,突出显示核心数据;
  • 开关控件联动SwitchListTile 与指令下发功能联动,实现远程控制设备功能。

4. 权限与蓝牙状态管理

  • 权限申请封装PermissionUtil 统一申请蓝牙相关权限,兼容 Android/iOS 权限差异;
  • 蓝牙状态监听:通过 FlutterBluePlus.instance.state 监听蓝牙开关状态,及时提示用户开启蓝牙;
  • 异常处理:连接失败、指令发送失败时通过 SnackBar 提示用户,提升交互体验。

五、运行与调试

1. 硬件准备

  • 需准备一台支持 BLE 通信的温湿度采集设备(如基于 ESP32 的开发板);
  • 硬件端需实现温湿度数据采集,并按照约定的 UUID 和协议广播数据;
  • 确保硬件端与手机端蓝牙处于同一频段,且设备未被其他设备占用。

2. 运行步骤

  1. 启动 Flutter 应用,进入设备扫描页面;
  2. 应用自动申请蓝牙权限,授权后开始扫描周边设备;
  3. 在设备列表中选择目标设备,点击连接;
  4. 连接成功后进入控制页面,实时查看温湿度数据和趋势图;
  5. 切换「报警阈值开关」,向设备发送控制指令。

六、进阶优化方向

1. 功能扩展

  • 多设备管理:支持同时连接多个设备,通过底部导航栏切换;
  • 数据历史记录:使用 sqflite 存储历史数据,支持查看历史趋势;
  • 报警通知:当温湿度超过阈值时,通过 flutter_local_notifications 发送本地通知;
  • 远程控制扩展:增加更多控制指令(如校准传感器、设置采样频率)。

2. 性能优化

  • 数据防抖:硬件端可能高频发送数据,软件端可添加防抖逻辑,避免 UI 频繁刷新;
  • 断开重连机制:监听设备断开事件,自动尝试重连;
  • 低功耗模式适配:Android/iOS 低功耗模式下蓝牙可能被关闭,需添加保活逻辑。

3. 跨端适配

  • 桌面端支持:Flutter 桌面端已支持蓝牙插件,可直接编译为 Windows/macOS 应用;
  • Web 端支持:通过 web_bluetooth 插件实现浏览器端蓝牙通信;
  • 多语言适配:添加中英文切换,适配国际化需求。

七、总结

本文以智能温湿度设备控制为场景,完整实现了 Flutter IoT 应用的开发流程,核心技术点包括BLE 蓝牙通信实时数据监控跨页面状态管理数据可视化。Flutter 凭借跨平台优势,可快速实现移动端与智能硬件的交互,大幅降低多端开发成本。

该案例可作为 IoT 应用开发的基础模板,通过扩展硬件协议和功能模块,可适配智能家居、工业监测、健康穿戴等多种 IoT 场景。

https://openharmonycrossplatform.csdn.net/content

Logo

开源鸿蒙跨平台开发社区汇聚开发者与厂商,共建“一次开发,多端部署”的开源生态,致力于降低跨端开发门槛,推动万物智联创新。

更多推荐