Flutter IoT 设备控制应用实战:蓝牙通信、实时数据监控与跨端适配
·
随着物联网(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>
二、核心功能拆解
本应用实现智能温湿度设备的全流程交互:
- 蓝牙权限申请:检测并申请蓝牙、位置(Android 需位置权限扫描设备)权限;
- 设备扫描与连接:扫描周边 BLE 设备,显示设备列表并支持连接;
- 实时数据监控:监听设备温湿度数据,实时更新 UI 与趋势图表;
- 远程指令下发:向设备发送控制指令(如开启 / 关闭报警阈值);
- 连接记录保存:保存最近连接的设备,下次启动快速重连。
三、完整代码实现
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. 运行步骤
- 启动 Flutter 应用,进入设备扫描页面;
- 应用自动申请蓝牙权限,授权后开始扫描周边设备;
- 在设备列表中选择目标设备,点击连接;
- 连接成功后进入控制页面,实时查看温湿度数据和趋势图;
- 切换「报警阈值开关」,向设备发送控制指令。
六、进阶优化方向
1. 功能扩展
- 多设备管理:支持同时连接多个设备,通过底部导航栏切换;
- 数据历史记录:使用
sqflite存储历史数据,支持查看历史趋势; - 报警通知:当温湿度超过阈值时,通过
flutter_local_notifications发送本地通知; - 远程控制扩展:增加更多控制指令(如校准传感器、设置采样频率)。
2. 性能优化
- 数据防抖:硬件端可能高频发送数据,软件端可添加防抖逻辑,避免 UI 频繁刷新;
- 断开重连机制:监听设备断开事件,自动尝试重连;
- 低功耗模式适配:Android/iOS 低功耗模式下蓝牙可能被关闭,需添加保活逻辑。
3. 跨端适配
- 桌面端支持:Flutter 桌面端已支持蓝牙插件,可直接编译为 Windows/macOS 应用;
- Web 端支持:通过
web_bluetooth插件实现浏览器端蓝牙通信; - 多语言适配:添加中英文切换,适配国际化需求。
七、总结
本文以智能温湿度设备控制为场景,完整实现了 Flutter IoT 应用的开发流程,核心技术点包括BLE 蓝牙通信、实时数据监控、跨页面状态管理和数据可视化。Flutter 凭借跨平台优势,可快速实现移动端与智能硬件的交互,大幅降低多端开发成本。
该案例可作为 IoT 应用开发的基础模板,通过扩展硬件协议和功能模块,可适配智能家居、工业监测、健康穿戴等多种 IoT 场景。
更多推荐


所有评论(0)