Library / SDK
PhilipsHue/flutter_reactive_ble avatar
PhilipsHue/flutter_reactive_ble

flutter_reactive_ble wraps Bluetooth Low Energy in Dart streams

Flutter library that handles BLE operations for multiple devices.

740 stars402 forksDartNOASSERTION

At a glance

What is it?
flutter_reactive_ble is a Flutter plugin that scans for BLE devices, holds several connections at once and exposes reads, writes and notifications as Dart streams. The API is compact, but the real cost of adopting it sits in platform permissions and in a custom licence that GitHub cannot classify.
Who is it for?
flutter_reactive_ble is a reasonable choice for a Flutter app that talks to multiple BLE peripherals and wants one Dart API across Android, iOS and macOS instead of hand-written platform channels. It is the wrong tool for a project that needs a published licence classification, for teams that cannot add Android runtime permissions or iOS usage descriptions, or for anything outside the BLE feature set the plugin covers.
Can I use it commercially?
Check first. The repository uses a licence we do not classify automatically, so read its LICENSE file before any commercial use.
Is it still maintained?
Yes. The repository last received commits 1 day ago.
What is it written in?
Mainly Dart, according to GitHub's language statistics.

Answers come from the project's GitHub data, last synced on October 23, 2026, and from our analysis. They are not legal advice.

Editorial analysis

One Dart API for scanning, connecting and characteristics

flutter_reactive_ble is a Flutter library that handles BLE operations for multiple devices. The scope is broader than a single-purpose wrapper: the plugin covers device discovery, observing the host device's BLE status, establishing a connection, keeping connection status for several devices, discovering services, reading and writing characteristics, subscribing to characteristics, clearing the GATT cache and negotiating the MTU size.

The API style is streams rather than futures for anything ongoing. Scanning returns a stream of discovered devices, a connection returns a stream of connection states, and the host BLE status arrives on a stream as well. That fits the shape of BLE work, where the interesting events keep arriving after the initial handshake, and it lets a Flutter widget rebuild from a subscription without polling.

The repository is a monorepo. The top-level listing shows a packages directory alongside melos.yaml, and the README links into packages/reactive_ble_platform_interface for the BleStatus definition, which shows that the platform interface is a separate package from the plugin itself. The example app under example/ carries its own pubspec, Android, iOS and macOS folders, so the repository doubles as a reference for how the permissions and the API calls fit together.

Android permissions are the first thing you have to get right

The README is explicit that the Android setup requires manifest entries. Without them the plugin cannot reach the radio, and the failure mode on recent Android versions is a silent scan that returns nothing:

xml
<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.ACCESS_FINE_LOCATION" android:maxSdkVersion="30" />
<uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION" android:maxSdkVersion="30" />

The location permissions are capped at API level 30 because the newer Bluetooth permissions replaced the location-based model. If your app also needs real location services, the README tells you to remove the android:maxSdkVersion="30" attribute from the location tags, otherwise the platform will not grant location access on newer versions.

There is a second case worth planning for. If the app derives location from the Bluetooth scan itself, the neverForLocation flag has to be removed, which means the scan can be used to infer position and the platform requires the extra declaration:

xml
 <uses-permission android:name="android.permission.BLUETOOTH_SCAN" 
                     tools:remove="android:usesPermissionFlags"
                     tools:targetApi="s" />

The Kotlin version is pinned as well: the library requires kotlin version 2.4.10, and the README describes updating it through the Kotlin plugin update channel in Android Studio set to 2.4.x. A Flutter app that still targets an older Kotlin plugin will fail to build against the plugin, which makes this a dependency to check before writing any Dart.

ProGuard and the Info.plist are the two platform gates

Two smaller platform requirements are easy to miss. On Android, a ProGuard build can strip classes the plugin needs, which the README ties to issue #131. The fix is a keep rule in proguard-rules.pro that preserves every member of the com.signify.hue package classes.

On iOS and macOS, Core Bluetooth refuses to work without a usage description in the app's Info.plist. The README lists NSBluetoothAlwaysUsageDescription as the required entry and points to the example app's Info.plist for the exact form. For macOS it also says to enable Bluetooth from the app's Capabilities.

Neither requirement is the plugin's fault, but both sit on the critical path: a build that minifies, or an iOS target that ships without the plist entry, fails at runtime in ways that look like plugin bugs. The com.signify.hue package name also tells you the lineage, since the plugin comes from the Philips Hue side of the company and the keep rule is scoped to those classes.

Scanning with a service filter and a platform-specific scan mode

Discovery is a stream, filtered by the advertised service UUIDs:

dart
flutterReactiveBle.scanForDevices(withServices: [serviceId], scanMode: ScanMode.lowLatency).listen((device) {
      //code for handling results
    }, onError: () {
      //code for handling error
    });

The withServices parameter takes the advertised service IDs to look for, and an empty list means every advertising device is reported. The scanMode parameter is the one cross-platform trap in this API: it is only used on Android and follows the conventions of the Android ScanSettings reference, where lowLatency trades battery for faster discovery. If scanMode is omitted, the balanced scan mode applies.

The host device status has its own stream, which is the correct way to check whether Bluetooth is switched on or whether the runtime permissions were granted:

dart
_ble.statusStream.listen((status) {
  //code for handling status update
});

There is also a synchronous _ble.status property for the current value. The BleStatus model referenced from the platform interface package defines what each status means.

Device identifiers differ across platforms, and Android connections hang

Connecting takes a device id, the services you want discovered, and an optional timeout:

dart
flutterReactiveBle.connectToDevice(
      id: foundDeviceId,
      servicesWithCharacteristicsToDiscover: {serviceId: [char1, char2]},
      connectionTimeout: const Duration(seconds: 2),
    ).listen((connectionState) {
      // Handle connection state updates
    }, onError: (Object error) {
      // Handle a possible error
    });

The id comes from discovery, and what it looks like depends on the platform. On iOS it is a UUID, on Android it is a MAC address, and that MAC address may itself be randomized depending on the Android version. Code that persists device identifiers therefore has to cope with two shapes, and on Android it cannot assume a stable identity across runs.

The servicesWithCharacteristicsToDiscover map is worth filling in on iOS. Passing it speeds up the connection, otherwise all services and characteristics are discovered. The connectionTimeout gives you an error instead of an indefinite wait when the peripheral does not answer.

The README calls out a specific Android failure: the Android BLE stack can hang when you try to connect to a device that is out of range. The workaround is a separate method that scans first and only connects if the device is found:

dart
flutterReactiveBle.connectToAdvertisingDevice(
    id: foundDeviceId,
    withServices: [serviceUuid],
    prescanDuration: const Duration(seconds: 5),
    servicesWithCharacteristicsToDiscover: {serviceId: [char1, char2]},
    connectionTimeout: const Duration(seconds:  2),
  ).listen((connectionState) {
    // Handle connection state updates
  }, onError: (dynamic error) {
    // Handle a possible error
  });

That variant requires withServices and prescanDuration in addition to the normal parameters, and prescanDuration is how long the stack listens for the device before attempting the connection. If your product runs on Android and hits unexplained connection stalls, this is the first thing to try.

Reads and writes address a qualified characteristic

Reading a characteristic means naming the service, the characteristic and the device, then awaiting the response:

dart
final characteristic = QualifiedCharacteristic(serviceId: serviceUuid, characteristicId: characteristicUuid, deviceId: foundDeviceId);
final response = await flutterReactiveBle.readCharacteristic(characteristic);

The qualified name matters because a characteristic UUID alone is ambiguous: the same characteristic id can appear under more than one service, and the same id can exist on different peripherals. Qualifying it with the service and the device removes the ambiguity, which is the kind of small design decision that saves time later.

Writing has two forms. A write with response awaits an acknowledgement of reception, so the call either succeeds or throws an exception, while a write without response does not wait for the peripheral to confirm. The feature list also includes subscribing to a characteristic, clearing the GATT cache, and negotiating the MTU size, which together cover the usual needs of a peripheral that pushes data back to the phone.

Service discovery itself is implicit in this design: you pass the services and characteristics you care about when connecting, rather than walking the GATT tree yourself.

A custom licence and a release trail that lags the commits

Two facts belong in any adoption review. First, the licence. The licence recorded for the repository is a custom one that GitHub cannot classify, and a LICENSE file sits at the repository root. A licence that tooling cannot classify means no automated clearance, so whoever owns licensing in your organisation has to read that file and decide. That is a licence fact rather than legal advice, but it is a step that cannot be skipped.

Second, the release cadence. The repository is not archived and the last push was on 2026-08-24, so work continues. The published releases are v5.4.0 from 2025-02-07, 5.3.1 from 2024-02-05 and 5.3.0 from 2024-01-25, so the newest tag is well behind the tip of the branch. Depending on which version you take, you may be months away from the fixes that exist in the source tree, and the Kotlin version requirement can differ between them.

For contributors, the setup uses melos to manage the packages in the repository. Install melos with dart pub global activate melos and point it at your local checkout with melos bootstrap. The repository also carries an analysis_options.yaml, a .swiftlint.yml and a bin directory, which points to Dart static analysis, Swift linting and some command line tooling.

Where the plugin stops and platform channels take over

The honest alternative to this plugin is not another BLE library, it is writing the platform channel layer yourself. flutter_reactive_ble wraps the native Android and iOS BLE stacks behind a Dart interface with a stable API surface, error handling and stream semantics. A hand-written integration gives you direct access to every native call the platform offers, including vendor specific extensions that no cross-platform wrapper exposes, and it removes a dependency. What you give up is the shared abstraction across Android, iOS and macOS, the connection-state handling, and the work of dealing with the Android stack hang that the connectToAdvertisingDevice path papers over.

There are also tasks this plugin is plainly not for. It does not manage pairing or bond storage, it does not provide a background or foreground service for continuous scanning on Android, and it does not handle the advertising side, so it is only half of a beacon interaction. Its feature list ends at the GATT level.

The other constraint is platform policy. If your application cannot request the Bluetooth permissions listed in the manifest, or cannot ship a usage description on iOS, then no amount of plugin work changes the outcome. Check those platform requirements against your product constraints first, because they are the cheapest thing to verify and the most common reason a BLE feature never ships.

Editorial conclusion

flutter_reactive_ble is a reasonable choice for a Flutter app that talks to multiple BLE peripherals and wants one Dart API across Android, iOS and macOS instead of hand-written platform channels. It is the wrong tool for a project that needs a published licence classification, for teams that cannot add Android runtime permissions or iOS usage descriptions, or for anything outside the BLE feature set the plugin covers. Before adopting it, read the LICENSE file at the repository root, because GitHub records the licence as a custom one it cannot classify, and check that the release you pull matches the Kotlin version your Android build expects.

Frequently asked questions

Which Android permissions does flutter_reactive_ble need?

The README lists BLUETOOTH_SCAN, BLUETOOTH_CONNECT and location permissions capped at API level 30 in AndroidManifest.xml. If you use location services, remove the android:maxSdkVersion="30" attribute from the location tags.

What should I add to Info.plist for flutter_reactive_ble on iOS and macOS?

iOS and macOS need the NSBluetoothAlwaysUsageDescription entry in Info.plist, since Core Bluetooth access is not allowed without it. On macOS you also need to enable Bluetooth from the app Capabilities.

Why does my Android connection to a BLE device hang with flutter_reactive_ble?

The README notes that the Android BLE stack can hang when connecting to a device that is out of range. Use connectToAdvertisingDevice, which scans for the device for a prescanDuration first and only connects if it is found.

What device id do I pass to connectToDevice in flutter_reactive_ble?

Use an id obtained through device discovery. On iOS the device id is a UUID, on Android it is a MAC address, and that MAC address may be randomized depending on the Android version.

What licence does flutter_reactive_ble use?

The repository records a custom licence that GitHub cannot classify, and the full text is in the LICENSE file at the repository root. Review that file yourself, because automated licence clearance tools will not resolve it.

Official sources

  1. Issues
  2. PhilipsHue/flutter_reactive_ble on GitHub
  3. Project website
  4. README
  5. Releases
Add this badge to your README

If you maintain this project, the badge below links readers to this analysis and shows its maintenance status from the daily GitHub snapshot. Paste the markdown into your README; add ?metric=license or ?metric=stars to the image URL for a different field.

Add this badge to your README

markdown
[![Hysen Labs](https://hysenlabs.com/badge/philipshue-flutter-reactive-ble.svg)](https://hysenlabs.com/projects/philipshue-flutter-reactive-ble)

Was this useful?