Flutter BLE蓝牙开发避坑指南:14年硬件APP经验总结

0 阅读1分钟

Flutter BLE蓝牙开发避坑指南:14年硬件APP经验总结

做了14年移动端开发,期间做过扫地机器人、割草机、运动相机的蓝牙连接APP。

BLE这个东西,每次接新项目都能踩到新坑。这篇文章把我遇到过的高频问题和解决方案整理出来,希望能帮到正在做智能硬件配套APP的开发者。


坑1:连接成功但特征值找不到

很多人第一次做BLE,连接成功后直接去读写,结果报错找不到特征值。

原因:  连接成功 ≠ 服务已发现。必须先调用 discoverServices()

await device.connect();

// ❌ 错误:连接后直接操作
// await characteristic.read();

// ✅ 正确:先发现服务
final services = await device.discoverServices();
for (final service in services) {
  for (final char in service.characteristics) {
    print('UUID: ${char.uuid}');
  }
}

坑2:断线后不会自动重连

BLE连接非常脆弱,手机离设备远一点、切个后台、锁屏,都可能断线。

很多项目只处理了连接,没有处理断线重连,用户体验很差。

正确做法:指数退避重连

// 退避时间:1s → 2s → 5s → 10s → 30s
static const _delays = [
  Duration(seconds: 1),
  Duration(seconds: 2),
  Duration(seconds: 5),
  Duration(seconds: 10),
  Duration(seconds: 30),
];

void _scheduleReconnect(BluetoothDevice device) {
  // 检查是否仍需重连(用户可能已手动断开)
  if (!_autoReconnectConfigs.containsKey(device.remoteId.str)) return;

  Future.delayed(config.nextDelay(), () async {
    try {
      await connect(device);
    } catch (_) {
      // 本次失败,等待下次调度
    }
  });
}

注意两个细节:

  • 重连前要检查用户是否已主动断开,否则会出现用户断开后反复自动重连的问题
  • 重连成功后要重置退避计数,避免下次断线等待时间过长

坑3:Android 权限在不同版本行为不一致

Android 12(API 31)之前和之后,蓝牙权限体系完全不同:

<!-- Android 12+ 需要这两个 -->
<uses-permission android:name="android.permission.BLUETOOTH_SCAN"
    android:usesPermissionFlags="neverForLocation" />
<uses-permission android:name="android.permission.BLUETOOTH_CONNECT" />

<!-- Android 11 及以下需要这两个 -->
<uses-permission android:name="android.permission.BLUETOOTH"
    android:maxSdkVersion="30" />
<uses-permission android:name="android.permission.BLUETOOTH_ADMIN"
    android:maxSdkVersion="30" />

<!-- 所有版本都需要位置权限才能扫描 -->
<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" />

代码里统一处理:

static Future<bool> requestAndroidPermissions() async {
  final results = await [
    Permission.bluetoothScan,
    Permission.bluetoothConnect,
    Permission.locationWhenInUse,
  ].request();
  return results.values.every((s) => s.isGranted);
}

坑4:收到的数据不知道怎么解析

设备返回的是原始字节 [0x1A, 0x2B, 0x00, 0x64],怎么变成有意义的数值?

// bytes → Int16(小端)
static int toInt16(List<int> bytes, {int offset = 0}) {
  final bd = ByteData.sublistView(
    Uint8List.fromList(bytes.sublist(offset, offset + 2))
  );
  return bd.getInt16(0, Endian.little);
}

// bytes → HEX字符串(调试用)
static String toHexString(List<int> bytes) =>
    bytes.map((b) => b.toRadixString(16).padLeft(2, '0').toUpperCase())
         .join(' ');

// 提取标志位
static bool getBit(int byte, int position) =>
    (byte >> position) & 1 == 1;

解析协议帧(大多数硬件设备都有自己的帧格式):

// 帧格式示例:[AA][长度][命令][数据...][XOR校验]
static BLEFrame? parseFrame(List<int> bytes) {
  if (bytes.length < 4) return null;
  if (bytes[0] != 0xAA) return null;  // 帧头校验

  final dataLen = bytes[1];
  if (bytes.length < dataLen + 4) return null;  // 包不完整

  final checksum = bytes.sublist(0, 3 + dataLen)
      .reduce((a, b) => a ^ b) & 0xFF;
  if (checksum != bytes[3 + dataLen]) return null;  // 校验失败

  return BLEFrame(command: bytes[2], data: bytes.sublist(3, 3 + dataLen));
}

坑5:iOS后台断连

iOS默认会在APP切到后台后断开BLE连接,需要在 Info.plist 声明后台模式:

<key>UIBackgroundModes</key>
<array>
    <string>bluetooth-central</string>
</array>

光声明还不够,还要在连接时设置正确的选项,否则锁屏后还是可能断:

await device.connect(
  timeout: const Duration(seconds: 35),
  autoConnect: false,  // 手动控制,不依赖系统autoConnect
);

坑6:同时连多个设备管理混乱

用 Map 统一管理,以 deviceId 为 key:

final Map<String, BluetoothDevice> _connectedDevices = {};
final Map<String, StreamSubscription> _connectionSubs = {};

// 连接时注册
_connectedDevices[device.remoteId.str] = device;

// 断开时清理
_connectedDevices.remove(id);
_connectionSubs[id]?.cancel();
_connectionSubs.remove(id);

Riverpod 状态管理接入

用 StreamProvider 把 BLE 状态和 UI 解耦:

// 扫描结果
final scanResultsProvider = StreamProvider<List<ScanResult>>((ref) {
  return ref.watch(bleManagerProvider).scanResults;
});

// 设备连接状态(按设备区分)
final connectionStateProvider = StreamProvider.family<
    BluetoothConnectionState, BluetoothDevice>((ref, device) {
  return ref.watch(bleManagerProvider).connectionStateOf(device);
});

// UI里使用
ref.watch(scanResultsProvider).when(
  data: (results) => ListView(...),
  loading: () => CircularProgressIndicator(),
  error: (e, _) => Text('$e'),
);

总结

BLE开发的核心难点不在于API调用,而在于:

  1. 连接状态管理:正确处理断线、重连、多设备
  2. 权限兼容:iOS和Android两套完全不同的体系
  3. 数据解析:根据硬件协议正确解析字节流
  4. 后台保活:平台限制需要额外配置

把以上这些处理好,BLE项目就稳了。


上面这些逻辑我整理成了一套完整的Flutter BLE模板,flutter_blue_plus + Riverpod,可以直接集成进项目:

👉 Flutter BLE完整开发模板(68元)
afdian.com/item/51528c…

包含本文所有代码的完整实现,以及WiFi AP模式配网、完整UI示例。有问题留言。