Flutter 蓝牙开发指南 (flutter_blue)
flutter_blue
是 Flutter 中用于蓝牙低功耗(BLE)开发的流行插件。以下是完整的实现指南:
1. 添加依赖和配置
1.1 添加依赖
在 pubspec.yaml
中添加:
dependencies:
flutter_blue: ^0.8.0 # 请使用最新版本
permission_handler: ^10.2.0 # 用于权限管理
运行 flutter pub get
1.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.ACCESS_FINE_LOCATION"/>
<uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION"/>
<!-- Android 12+ 需要 -->
<uses-permission android:name="android.permission.BLUETOOTH_SCAN" android:usesPermissionFlags="neverForLocation" />
<uses-permission android:name="android.permission.BLUETOOTH_CONNECT" />
- 在
android/app/build.gradle
中设置 minSdkVersion 至少为 21
iOS 配置
在 ios/Runner/Info.plist
中添加:
<key>NSBluetoothAlwaysUsageDescription</key>
<string>需要蓝牙权限以连接设备</string>
<key>NSBluetoothPeripheralUsageDescription</key>
<string>需要蓝牙权限以连接设备</string>
2. 基本使用
2.1 初始化蓝牙适配器
import 'package:flutter_blue/flutter_blue.dart';
FlutterBlue flutterBlue = FlutterBlue.instance;
Future<void> initBluetooth() async {
// 检查蓝牙是否可用
bool isAvailable = await flutterBlue.isAvailable;
if (!isAvailable) {
print("蓝牙不可用");
return;
}
// 检查蓝牙是否开启
bool isOn = await flutterBlue.isOn;
if (!isOn) {
print("蓝牙未开启");
// 可以提示用户开启蓝牙
// 在Android上可以跳转到蓝牙设置
// 在iOS上无法以编程方式开启蓝牙
return;
}
}
2.2 扫描蓝牙设备
List<ScanResult> scanResults = [];
StreamSubscription? scanSubscription;
void startScan() {
// 停止之前的扫描
stopScan();
// 请求位置权限 (Android需要)
if (await Permission.location.request().isGranted) {
scanSubscription = flutterBlue.scan(
timeout: Duration(seconds: 10), // 扫描超时时间
).listen((scanResult) {
// 发现设备
setState(() {
scanResults.add(scanResult);
});
}, onError: (error) {
print("扫描错误: $error");
});
}
}
void stopScan() {
scanSubscription?.cancel();
scanSubscription = null;
flutterBlue.stopScan();
}
2.3 显示扫描结果
ListView.builder(
itemCount: scanResults.length,
itemBuilder: (context, index) {
ScanResult result = scanResults[index];
BluetoothDevice device = result.device;
return ListTile(
title: Text(device.name.isEmpty ? '未知设备' : device.name),
subtitle: Text(device.id.toString()),
trailing: Text(result.rssi.toString()),
onTap: () => connectToDevice(device),
);
},
)
3. 设备连接与通信
3.1 连接设备
BluetoothDevice? connectedDevice;
StreamSubscription? connectionSubscription;
Future<void> connectToDevice(BluetoothDevice device) async {
// 断开现有连接
if (connectedDevice != null) {
await disconnectFromDevice();
}
connectionSubscription = device.state.listen((state) {
print("设备状态: $state");
if (state == BluetoothDeviceState.connected) {
setState(() {
connectedDevice = device;
});
discoverServices(device);
} else if (state == BluetoothDeviceState.disconnected) {
setState(() {
connectedDevice = null;
});
}
});
try {
await device.connect(autoConnect: false, timeout: Duration(seconds: 15));
print("连接成功");
} catch (e) {
print("连接失败: $e");
}
}
Future<void> disconnectFromDevice() async {
if (connectedDevice != null) {
await connectedDevice!.disconnect();
connectionSubscription?.cancel();
connectionSubscription = null;
setState(() {
connectedDevice = null;
});
}
}
3.2 发现服务
List<BluetoothService> services = [];
Future<void> discoverServices(BluetoothDevice device) async {
try {
services = await device.discoverServices();
setState(() {});
print("发现 ${services.length} 个服务");
} catch (e) {
print("发现服务失败: $e");
}
}
3.3 读取特征值
Future<void> readCharacteristic(BluetoothCharacteristic characteristic) async {
try {
List<int> value = await characteristic.read();
print("特征值: $value");
// 处理读取到的数据
} catch (e) {
print("读取特征值失败: $e");
}
}
3.4 写入特征值
Future<void> writeCharacteristic(
BluetoothCharacteristic characteristic,
List<int> value,
) async {
try {
await characteristic.write(value);
print("写入成功");
} catch (e) {
print("写入失败: $e");
}
}
3.5 监听通知
StreamSubscription? notifySubscription;
void listenToCharacteristic(BluetoothCharacteristic characteristic) {
notifySubscription?.cancel();
notifySubscription = characteristic.value.listen((value) {
print("收到通知: $value");
// 处理通知数据
});
characteristic.setNotifyValue(true);
}
void stopListening() {
notifySubscription?.cancel();
notifySubscription = null;
}
4. 完整示例
import 'package:flutter/material.dart';
import 'package:flutter_blue/flutter_blue.dart';
import 'package:permission_handler/permission_handler.dart';
void main() => runApp(MyApp());
class MyApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
return MaterialApp(
title: '蓝牙示例',
theme: ThemeData(primarySwatch: Colors.blue),
home: BluetoothScreen(),
);
}
}
class BluetoothScreen extends StatefulWidget {
@override
_BluetoothScreenState createState() => _BluetoothScreenState();
}
class _BluetoothScreenState extends State<BluetoothScreen> {
FlutterBlue flutterBlue = FlutterBlue.instance;
List<ScanResult> scanResults = [];
StreamSubscription? scanSubscription;
BluetoothDevice? connectedDevice;
StreamSubscription? connectionSubscription;
List<BluetoothService> services = [];
StreamSubscription? notifySubscription;
@override
void initState() {
super.initState();
initBluetooth();
}
@override
void dispose() {
stopScan();
disconnectFromDevice();
stopListening();
super.dispose();
}
Future<void> initBluetooth() async {
bool isAvailable = await flutterBlue.isAvailable;
if (!isAvailable) {
print("蓝牙不可用");
return;
}
bool isOn = await flutterBlue.isOn;
if (!isOn) {
print("蓝牙未开启");
return;
}
}
void startScan() {
stopScan();
Permission.location.request().then((status) {
if (status.isGranted) {
scanSubscription = flutterBlue.scan(timeout: Duration(seconds: 10))
.listen((scanResult) {
setState(() {
if (!scanResults.any((result) => result.device.id == scanResult.device.id)) {
scanResults.add(scanResult);
}
});
}, onError: (error) {
print("扫描错误: $error");
});
}
});
}
void stopScan() {
scanSubscription?.cancel();
scanSubscription = null;
flutterBlue.stopScan();
}
Future<void> connectToDevice(BluetoothDevice device) async {
if (connectedDevice != null) {
await disconnectFromDevice();
}
connectionSubscription = device.state.listen((state) {
print("设备状态: $state");
if (state == BluetoothDeviceState.connected) {
setState(() {
connectedDevice = device;
});
discoverServices(device);
} else if (state == BluetoothDeviceState.disconnected) {
setState(() {
connectedDevice = null;
services.clear();
});
}
});
try {
await device.connect(autoConnect: false, timeout: Duration(seconds: 15));
print("连接成功");
} catch (e) {
print("连接失败: $e");
}
}
Future<void> disconnectFromDevice() async {
if (connectedDevice != null) {
await connectedDevice!.disconnect();
connectionSubscription?.cancel();
connectionSubscription = null;
setState(() {
connectedDevice = null;
services.clear();
});
}
}
Future<void> discoverServices(BluetoothDevice device) async {
try {
List<BluetoothService> discoveredServices = await device.discoverServices();
setState(() {
services = discoveredServices;
});
print("发现 ${services.length} 个服务");
} catch (e) {
print("发现服务失败: $e");
}
}
void listenToCharacteristic(BluetoothCharacteristic characteristic) {
stopListening();
notifySubscription = characteristic.value.listen((value) {
print("收到通知: $value");
});
characteristic.setNotifyValue(true);
}
void stopListening() {
notifySubscription?.cancel();
notifySubscription = null;
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: Text('蓝牙示例')),
body: SingleChildScrollView(
child: Column(
children: [
_buildControlPanel(),
_buildScanResults(),
_buildConnectedDeviceInfo(),
_buildServicesList(),
],
),
),
);
}
Widget _buildControlPanel() {
return Card(
child: Padding(
padding: const EdgeInsets.all(8.0),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
children: [
ElevatedButton(
onPressed: startScan,
child: Text('开始扫描'),
),
ElevatedButton(
onPressed: stopScan,
child: Text('停止扫描'),
),
if (connectedDevice != null)
ElevatedButton(
onPressed: disconnectFromDevice,
child: Text('断开连接'),
),
],
),
),
);
}
Widget _buildScanResults() {
return ExpansionTile(
title: Text('扫描结果 (${scanResults.length})'),
children: [
ListView.builder(
shrinkWrap: true,
physics: NeverScrollableScrollPhysics(),
itemCount: scanResults.length,
itemBuilder: (context, index) {
ScanResult result = scanResults[index];
BluetoothDevice device = result.device;
return ListTile(
title: Text(device.name.isEmpty ? '未知设备' : device.name),
subtitle: Text(device.id.toString()),
trailing: Text(result.rssi.toString()),
onTap: () => connectToDevice(device),
);
},
),
],
);
}
Widget _buildConnectedDeviceInfo() {
if (connectedDevice == null) return Container();
return Card(
child: Padding(
padding: const EdgeInsets.all(8.0),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text('已连接设备:', style: TextStyle(fontWeight: FontWeight.bold)),
Text('名称: ${connectedDevice!.name}'),
Text('ID: ${connectedDevice!.id}'),
],
),
),
);
}
Widget _buildServicesList() {
if (services.isEmpty) return Container();
return ExpansionTile(
title: Text('服务 (${services.length})'),
children: [
ListView.builder(
shrinkWrap: true,
physics: NeverScrollableScrollPhysics(),
itemCount: services.length,
itemBuilder: (context, serviceIndex) {
BluetoothService service = services[serviceIndex];
return ExpansionTile(
title: Text('服务: ${service.uuid}'),
children: service.characteristics.map((characteristic) {
return ListTile(
title: Text('特征: ${characteristic.uuid}'),
subtitle: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text('属性: ${_getProperties(characteristic.properties)}'),
if (characteristic.value != null)
Text('值: ${characteristic.value}'),
],
),
onTap: () {
if (characteristic.properties.notify) {
listenToCharacteristic(characteristic);
} else if (characteristic.properties.read) {
characteristic.read();
}
},
);
}).toList(),
);
},
),
],
);
}
String _getProperties(BluetoothCharacteristicProperties properties) {
List<String> props = [];
if (properties.broadcast) props.add('Broadcast');
if (properties.read) props.add('Read');
if (properties.writeWithoutResponse) props.add('WriteWithoutResponse');
if (properties.write) props.add('Write');
if (properties.notify) props.add('Notify');
if (properties.indicate) props.add('Indicate');
if (properties.authenticatedSignedWrites) props.add('AuthenticatedSignedWrites');
if (properties.extendedProperties) props.add('ExtendedProperties');
return props.join(', ');
}
}
5. 常见问题解决
- Android 上扫描不到设备:
- 确保已授予位置权限
- 检查设备是否可被发现
- 在 Android 6.0+ 需要运行时位置权限
- iOS 上连接失败:
- 确保在 Info.plist 中添加了蓝牙权限描述
- iOS 需要设备在蓝牙设置中配对
- 特征值操作失败:
- 检查特征是否支持该操作(读/写/通知)
- 确保设备已连接且服务已发现
- 跨平台差异:
- Android 和 iOS 在蓝牙实现上有差异
- 测试时应在两个平台上都进行验证
- 性能问题:
- 及时取消订阅和断开连接
- 避免频繁的蓝牙操作
6. 最佳实践
- 权限处理:
- 在操作前检查并请求必要权限
- 处理用户拒绝权限的情况
- 错误处理:
- 捕获所有可能的异常
- 提供用户友好的错误提示
- 资源管理:
- 及时取消订阅和释放资源
- 在页面销毁时断开连接
- 用户体验:
- 显示连接状态和操作反馈
- 提供重试机制
- 测试:
- 在不同设备和平台上测试
- 模拟各种异常情况
通过以上方法,你可以在 Flutter 应用中实现强大的蓝牙功能,与各种 BLE 设备进行交互。
正文完