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.