Skip to content
Blog

Bluetooth Low Energy in Flutter: Choosing and Using a BLE Package

Compare flutter_blue_plus 2.3.14 and flutter_reactive_ble 5.6.0, then scan, connect, discover services, subscribe, and handle Android and iOS BLE permissions.

Published on • November 5, 2026

AI Assistant

Bluetooth Low Energy looks simple until you fight the Android permission model or wonder why notifications never arrive. Two well-maintained plugins dominate pub.dev, and both are stream-based — the differences are platform reach, licensing and API taste.

The candidates on pub.dev

flutter_blue_plus is at 2.3.14, published October 3, 2026 by the verified jamcorder.com publisher. It covers Android, iOS, Linux, macOS and web (Windows comes from the companion flutter_blue_plus_winrt), has zero third-party dependencies beyond Flutter and the platform SDKs, and supports the BLE Central role only — Bluetooth Classic devices such as speakers, keyboards and HC-05 modules are explicitly unsupported. Important: since this line it ships under the FlutterBluePlus License, which is free for personal, nonprofit and educational use but requires a commercial license for for-profit use.

flutter_reactive_ble is at 5.6.0, published September 23, 2026 by the verified meethue.com (Philips Hue) publisher, BSD-3-Clause, for Android and iOS only. It handles multiple simultaneous connections and exposes statusStream so you can wait for BleStatus.ready before touching the stack.

A pub.dev search for bleak returns no Flutter BLE plugin of that name — pub.dev/packages/bleak is a 404, because “Bleak” belongs to the Python ecosystem. The real third option for Android/iOS is choosing between the two above.

Permissions: Android 12+ and iOS

Both plugins need the modern Android 12+ pair plus legacy fallbacks in AndroidManifest.xml:

<uses-feature android:name="android.hardware.bluetooth_le" android:required="false" />
<uses-permission android:name="android.permission.BLUETOOTH_SCAN"
    android:usesPermissionFlags="neverForLocation" />
<uses-permission android:name="android.permission.BLUETOOTH_CONNECT" />
<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" android:maxSdkVersion="30" />
<uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION" android:maxSdkVersion="28" />

Drop neverForLocation if you scan for iBeacons or derive location, and keep location services switched on — Android refuses BLE scans otherwise. On iOS, add NSBluetoothAlwaysUsageDescription to Info.plist (iOS 13+; NSBluetoothPeripheralUsageDescription for iOS 12 and lower). flutter_blue_plus also needs minSdkVersion 21, and release builds need -keep class com.lib.flutter_blue_plus.* { *; } in proguard-rules.pro to avoid Field androidScanMode_ ... not found crashes.

Scanning and connecting

flutter_blue_plus filters at the platform side and keeps results in streams:

if (await FlutterBluePlus.isSupported == false) return;

final resultsSub = FlutterBluePlus.onScanResults.listen((results) {
  if (results.isNotEmpty) print(results.last.device.remoteId);
});
FlutterBluePlus.cancelWhenScanComplete(resultsSub);

await FlutterBluePlus.adapterState
    .where((s) => s == BluetoothAdapterState.on)
    .first;
await FlutterBluePlus.startScan(
  withServices: [Guid('180D')],   // heart rate
  timeout: const Duration(seconds: 15),
);

flutter_reactive_ble takes the same shape with scanForDevices(withServices: [...], scanMode: ScanMode.lowLatency). Connect, then discover — and the README stresses you must call discoverServices() after every reconnection:

final device = results.last.device;
await device.connect();
final services = await device.discoverServices();

Its reactive counterpart is connectToDevice(id: foundDeviceId, connectionTimeout: const Duration(seconds: 2)).listen(...), where passing servicesWithCharacteristicsToDiscover speeds up iOS discovery considerably.

Services, characteristics and notifications

for (final service in services) {
  for (final c in service.characteristics) {
    if (c.properties.read) {
      final value = await c.read();
      print(value);
    }
    if (c.properties.notify) {
      device.cancelWhenDisconnected(
        c.onValueReceived.listen((v) => print('notify: $v')),
      );
      await c.setNotifyValue(true);
    }
  }
}

In flutter_reactive_ble the equivalent is a QualifiedCharacteristic(serviceId:, characteristicId:, deviceId:) passed to readCharacteristic, writeCharacteristicWithResponse or subscribeToCharacteristic. Note that if a characteristic supports both notifications and indications, flutter_blue_plus defaults to notifications, matching CoreBluetooth behaviour on iOS.

MTU and other real-world pitfalls

On Android, flutter_blue_plus requests an MTU of 512 during connect(); iOS and macOS negotiate automatically to roughly 135-255, always starting from 23. You can watch device.mtu or call device.requestMtu(512) (Android only). Beyond that: remoteId is a MAC address on Android but an iOS-generated UUID that periodically rotates, Android allows only five startScan calls per 30-second window, and ANDROID_SPECIFIC_ERROR has no 100% fix — catch and retry. For multi-device apps, call FlutterBluePlus.setOperationQueueMode(OperationQueueMode.perDevice) before any other work so devices do not serialize behind each other.

Takeaway: pick flutter_blue_plus 2.3.14 for the widest platform coverage (mind the commercial license) or flutter_reactive_ble 5.6.0 for a BSD-licensed multi-device API, then get BLUETOOTH_SCAN/BLUETOOTH_CONNECT and NSBluetoothAlwaysUsageDescription right before debugging a single byte of payload.