Note: this plugin is continuous work from FlutterBlue.
Migrating from FlutterBlue? See Migration Guides
- Introduction
- Usage
- Getting Started
- Using Ble in App Background
- Reference
- Debugging
- Mocking
- Common Problems
FlutterBluePlus is a Bluetooth Low Energy plugin for Flutter.
It supports BLE Central Role only (most common).
If you need BLE Peripheral Role, you should check out FlutterBlePeripheral.
i.e. speakers, headphones, mice, keyboards, gamepads, Arduino HC-05 & HC-06, and more are not supported. These all use Bluetooth Classic.
Also, iBeacons are not supported on iOS. Apple requires you to use CoreLocation.
FlutterBluePlus aims to offer the most from all supported platforms: iOS, macOS, Android.
The code is written to be simple, robust, and incredibly easy to understand.
FlutterBluePlus has zero dependencies besides Flutter, Android, and iOS themselves.
This makes FlutterBluePlus very stable, and easy to maintain.
Please star this repo & on pub.dev. We all benefit from having a larger community.
FlutterBluePlus has a beautiful example app, useful to debug issues.
cd ./example
flutter run
Flutter Blue Plus takes error handling very seriously.
Every error returned by the native platform is checked and thrown as an exception where appropriate. See Reference for a list of throwable functions.
Streams: Streams returned by FlutterBluePlus never emit any errors and never close. There's no need to handle onError
or onDone
for stream.listen(...)
. The one exception is FlutterBluePlus.scanResults
, which you should handle onError
.
// if your terminal doesn't support color you'll see annoying logs like `\x1B[1;35m`
FlutterBluePlus.setLogLevel(LogLevel.verbose, color:false)
Setting LogLevel.verbose
shows all data in and out.
⚫ = function name
🟣 = args to platform
🟡 = data from platform
Note: On iOS, a "This app would like to use Bluetooth" system dialogue appears on first call to any FlutterBluePlus method.
// first, check if bluetooth is supported by your hardware
// Note: The platform is initialized on the first call to any FlutterBluePlus method.
if (await FlutterBluePlus.isSupported == false) {
print("Bluetooth not supported by this device");
return;
}
// handle bluetooth on & off
// note: for iOS the initial state is typically BluetoothAdapterState.unknown
// note: if you have permissions issues you will get stuck at BluetoothAdapterState.unauthorized
FlutterBluePlus.adapterState.listen((BluetoothAdapterState state) {
print(state);
if (state == BluetoothAdapterState.on) {
// usually start scanning, connecting, etc
} else {
// show an error to the user, etc
}
});
// turn on bluetooth ourself if we can
// for iOS, the user controls bluetooth enable/disable
if (Platform.isAndroid) {
await FlutterBluePlus.turnOn();
}
If your device is not found, see Common Problems.
It is recommended to set scan filters to reduce main thread & platform channel usage.
// listen to scan results
// Note: `onScanResults` only returns live scan results, i.e. during scanning
// Use: `scanResults` if you want live scan results *or* the previous results
var subscription = FlutterBluePlus.onScanResults.listen((results) {
if (results.isNotEmpty) {
ScanResult r = results.last; // the most recently found device
print('${r.device.remoteId}: "${r.advertisementData.advName}" found!');
}
},
onError(e) => print(e);
);
// Wait for Bluetooth enabled & permission granted
// In your real app you should use `FlutterBluePlus.adapterState.listen` to handle all states
await FlutterBluePlus.adapterState.where((val) => val == BluetoothAdapterState.on).first;
// Start scanning
await FlutterBluePlus.startScan();
// Stop scanning
await FlutterBluePlus.stopScan();
// cancel to prevent duplicate listeners
subscription.cancel();
// listen for disconnection
var subscription = device.connectionState.listen((BluetoothConnectionState state) async {
if (state == BluetoothConnectionState.disconnected) {
// 1. typically, start a periodic timer that tries to
// reconnect, or just call connect() again right now
// 2. you must always re-discover services after disconnection!
print("${device.disconnectReasonCode} ${device.disconnectReasonDescription}");
}
});
// Connect to the device
await device.connect();
// Disconnect from device
await device.disconnect();
// cancel to prevent duplicate listeners
subscription.cancel();
Connects whenever your device is found.
// enable auto connect
// - this function always returns immediately
// - you must listen to `device.connectionState` to know when connection occurs
await device.connect(autoConnect:true)
// disable auto connect
await device.disconnect()
On Android, we request an mtu of 512 by default during connection (see: connect
function arguments).
On iOS & macOS, the mtu is negotiated automatically, typically 135 to 255.
final subscription = device.mtu.listen((int mtu) {
// iOS: initial value is always 23, but iOS will quickly negotiate a higher value
print("mtu $mtu");
});
// cleanup: cancel subscription when disconnected
device.cancelWhenDisconnected(subscription);
// You can also manually change the mtu yourself.
if (Platform.isAndroid) {
await device.requestMtu(512);
}
// Note: You must call discoverServices after every re-connection!
List<BluetoothService> services = await device.discoverServices();
services.forEach((service) {
// do something with service
});
// Reads all characteristics
var characteristics = service.characteristics;
for(BluetoothCharacteristic c in characteristics) {
if (c.properties.read) {
List<int> value = await c.read();
print(value);
}
}
// Writes to a characteristic
await c.write([0x12, 0x34]);
allowLongWrite: To write large characteristics (up to 512 bytes) regardless of mtu, use allowLongWrite
:
/// allowLongWrite should be used with caution.
/// 1. it can only be used *with* response to avoid data loss
/// 2. the peripheral device must support the 'long write' ble protocol.
/// 3. Interrupted transfers can leave the characteristic in a partially written state
/// 4. If the mtu is small, it is very very slow.
await c.write(data, allowLongWrite:true);
splitWrite: To write lots of data (unlimited), you can define the splitWrite
function.
import 'dart:math';
// split write should be used with caution.
// 1. due to splitting, `characteristic.read()` will return partial data.
// 2. it can only be used *with* response to avoid data loss
// 3. The characteristic must be designed to support split data
extension splitWrite on BluetoothCharacteristic {
Future<void> splitWrite(List<int> value, {int timeout = 15}) async {
int chunk = device.mtuNow - 3; // 3 bytes ble overhead
for (int i = 0; i < value.length; i += chunk) {
List<int> subvalue = value.sublist(i, min(i + chunk, value.length));
await write(subvalue, withoutResponse:false, timeout: timeout);
}
}
}
If onValueReceived
is never called, see Common Problems in the README.
final subscription = characteristic.onValueReceived.listen((value) {
// onValueReceived is updated:
// - anytime read() is called
// - anytime a notification arrives (if subscribed)
});
// cleanup: cancel subscription when disconnected
device.cancelWhenDisconnected(subscription);
// subscribe
// Note: If a characteristic supports both **notifications** and **indications**,
// it will default to **notifications**. This matches how CoreBluetooth works on iOS.
await characteristic.setNotifyValue(true);
lastValueStream
is an alternative to onValueReceived
. It emits a value any time the characteristic changes, including writes.
It is very convenient for simple characteristics that support both WRITE and READ (and/or NOTIFY). e.g. a "light switch toggle" characteristic.
final subscription = characteristic.lastValueStream.listen((value) {
// lastValueStream` is updated:
// - anytime read() is called
// - anytime write() is called
// - anytime a notification arrives (if subscribed)
// - also when first listened to, it re-emits the last value for convenience.
});
// cleanup: cancel subscription when disconnected
device.cancelWhenDisconnected(subscription);
// enable notifications
await characteristic.setNotifyValue(true);
// Reads all descriptors
var descriptors = characteristic.descriptors;
for(BluetoothDescriptor d in descriptors) {
List<int> value = await d.read();
print(value);
}
// Writes to a descriptor
await d.write([0x12, 0x34])
FlutterBluePlus automatically listens to the Services Changed Characteristic (0x2A05)
In FlutterBluePlus, we call it onServicesReset
because you must re-discover services.
// - uses the GAP Services Changed characteristic (0x2A05)
// - you must call discoverServices() again
device.onServicesReset.listen(() async {
print("Services Reset");
await device.discoverServices();
});
Get devices currently connected to your app.
List<BluetoothDevice> devs = FlutterBluePlus.connectedDevices;
for (var d in devs) {
print(d);
}
Get devices connected to the system by any app.
Note: you must connect your app to them before you can communicate with them.
List<BluetoothDevice> devs = await FlutterBluePlus.systemDevices;
for (var d in devs) {
await d.connect(); // Must connect *our* app to the device
await d.discoverServices();
}
Note: calling this is usually not necessary!! The platform will do it automatically.
However, you can force the popup to show sooner.
final bsSubscription = device.bondState.listen((value) {
print("$value prev:{$device.prevBondState}");
});
// cleanup: cancel subscription when disconnected
device.cancelWhenDisconnected(bsSubscription);
// Force the bonding popup to show now (Android Only)
await device.createBond();
// remove bond
await device.removeBond();
Access streams from all devices simultaneously.
There are streams for:
- events.onConnectionStateChanged
- events.onMtuChanged
- events.onReadRssi
- events.onServicesReset
- events.onDiscoveredServices
- events.onCharacteristicReceived
- events.onCharacteristicWritten
- events.onDescriptorRead
- events.onDescriptorWritten
- events.onNameChanged (iOS Only)
- events.onBondStateChanged (Android Only)
// listen to *any device* connection state changes
FlutterBluePlus.events.onConnectionStateChanged.listen((event)) {
print('${event.device} ${event.connectionState}');
}
To mock FlutterBluePlus
for development, refer to the Mocking Guide.
flutter_blue_plus is compatible only from version 21 of Android SDK so you should change this in android/app/build.gradle:
Android {
defaultConfig {
minSdkVersion: 21
In the android/app/src/main/AndroidManifest.xml add:
<!-- Tell Google Play Store that your app uses Bluetooth LE
Set android:required="true" if bluetooth is necessary -->
<uses-feature android:name="android.hardware.bluetooth_le" android:required="false" />
<!-- New Bluetooth permissions in Android 12
https://developer.android.com/about/versions/12/features/bluetooth-permissions -->
<uses-permission android:name="android.permission.BLUETOOTH_SCAN" android:usesPermissionFlags="neverForLocation" />
<uses-permission android:name="android.permission.BLUETOOTH_CONNECT" />
<!-- legacy for Android 11 or lower -->
<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"/>
<!-- legacy for Android 9 or lower -->
<uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION" android:maxSdkVersion="28" />
If you want to use Bluetooth to determine location.
In the android/app/src/main/AndroidManifest.xml add:
<!-- Tell Google Play Store that your app uses Bluetooth LE
Set android:required="true" if bluetooth is necessary -->
<uses-feature android:name="android.hardware.bluetooth_le" android:required="false" />
<!-- New Bluetooth permissions in Android 12
https://developer.android.com/about/versions/12/features/bluetooth-permissions -->
<uses-permission android:name="android.permission.BLUETOOTH_SCAN"/>
<uses-permission android:name="android.permission.BLUETOOTH_CONNECT" />
<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" />
<!-- legacy for Android 11 or lower -->
<uses-permission android:name="android.permission.BLUETOOTH" android:maxSdkVersion="30" />
<uses-permission android:name="android.permission.BLUETOOTH_ADMIN" android:maxSdkVersion="30" />
<!-- legacy for Android 9 or lower -->
<uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION" android:maxSdkVersion="28" />
And set androidUsesFineLocation when scanning:
// Start scanning
flutterBlue.startScan(timeout: Duration(seconds: 4), androidUsesFineLocation: true);
Add the following line in your project/android/app/proguard-rules.pro
file:
-keep class com.lib.flutter_blue_plus.* { *; }
to avoid seeing the following kind errors in your release
builds:
PlatformException(startScan, Field androidScanMode_ for m0.e0 not found. Known fields are
[private int m0.e0.q, private b3.b0$i m0.e0.r, private boolean m0.e0.s, private static final m0.e0 m0.e0.t,
private static volatile b3.a1 m0.e0.u], java.lang.RuntimeException: Field androidScanMode_ for m0.e0 not found
In the ios/Runner/Info.plist let’s add:
<dict>
<key>NSBluetoothAlwaysUsageDescription</key>
<string>This app always needs Bluetooth to function</string>
<key>NSBluetoothPeripheralUsageDescription</key>
<string>This app needs Bluetooth Peripheral to function</string>
<key>NSLocationAlwaysAndWhenInUseUsageDescription</key>
<string>This app always needs location and when in use to function</string>
<key>NSLocationAlwaysUsageDescription</key>
<string>This app always needs location to function</string>
<key>NSLocationWhenInUseUsageDescription</key>
<string>This app needs location when in use to function</string>
For location permissions on iOS see more at: https://developer.apple.com/documentation/corelocation/requesting_authorization_for_location_services
And in Xcode, add access to Bluetooth hardware:
Xcode -> Runners -> Targets -> Runner-> Signing & Capabilities -> App Sandbox -> Hardware -> Enable Bluetooth
This is an advanced use case. FlutterBluePlus does not support everything. You may have to fork it.
Add the following to your Info.plist
<key>UIBackgroundModes</key>
<array>
<string>bluetooth-central</string>
</array>
When this key-value pair is included in the app’s Info.plist file, the system wakes up your app to process ble read
, write
, and subscription
events.
Note: Upon being woken up, an app has around 10 seconds to complete a task. Apps that spend too much time executing in the background can be throttled back by the system or killed.
You can try using https://pub.dev/packages/flutter_foreground_task
🌀 = Stream ⚡ = synchronous
Android | iOS | Throws | Description | |
---|---|---|---|---|
setLogLevel | ✅ | ✅ | Configure plugin log level | |
isSupported | ✅ | ✅ | Checks whether the device supports Bluetooth | |
turnOn | ✅ | 🔥 | Turns on the bluetooth adapter | |
adapterState 🌀 | ✅ | ✅ | Stream of on & off states of the bluetooth adapter | |
startScan | ✅ | ✅ | 🔥 | Starts a scan for Ble devices |
stopScan | ✅ | ✅ | 🔥 | Stop an existing scan for Ble devices |
onScanResults 🌀 | ✅ | ✅ | Stream of live scan results | |
scanResults 🌀 | ✅ | ✅ | Stream of live scan results or previous results | |
lastScanResults ⚡ | ✅ | ✅ | The most recent scan results | |
isScanning 🌀 | ✅ | ✅ | Stream of current scanning state | |
isScanningNow ⚡ | ✅ | ✅ | Is a scan currently running? | |
connectedDevices ⚡ | ✅ | ✅ | List of devices connected to your app | |
systemDevices | ✅ | ✅ | 🔥 | List of devices connected to the system, even by other apps |
getPhySupport | ✅ | 🔥 | Get supported bluetooth phy codings |
Android | iOS | Throws | Description | |
---|---|---|---|---|
events.onConnectionStateChanged 🌀 | ✅ | ✅ | Stream of connection changes of all devices | |
events.onMtuChanged 🌀 | ✅ | ✅ | Stream of mtu changes of all devices | |
events.onReadRssi 🌀 | ✅ | ✅ | Stream of rssi reads of all devices | |
events.onServicesReset 🌀 | ✅ | ✅ | Stream of services resets of all devices | |
events.onDiscoveredServices 🌀 | ✅ | ✅ | Stream of services discovered of all devices | |
events.onCharacteristicReceived 🌀 | ✅ | ✅ | Stream of characteristic value reads of all devices | |
events.onCharacteristicWritten 🌀 | ✅ | ✅ | Stream of characteristic value writes of all devices | |
events.onDescriptorRead 🌀 | ✅ | ✅ | Stream of descriptor value reads of all devices | |
events.onDescriptorWritten 🌀 | ✅ | ✅ | Stream of descriptor value writes of all devices | |
events.onBondStateChanged 🌀 | ✅ | Stream of android bond state changes of all devices | ||
events.onNameChanged 🌀 | ✅ | Stream of iOS name changes of all devices |
Android | iOS | Throws | Description | |
---|---|---|---|---|
platformName ⚡ | ✅ | ✅ | The platform preferred name of the device | |
advName ⚡ | ✅ | ✅ | The advertised name of the device found during scanning | |
connect | ✅ | ✅ | 🔥 | Establishes a connection to the device |
disconnect | ✅ | ✅ | 🔥 | Cancels an active or pending connection to the device |
isConnected ⚡ | ✅ | ✅ | Is this device currently connected to your app? | |
connectionState 🌀 | ✅ | ✅ | Stream of connection changes for the Bluetooth Device | |
discoverServices | ✅ | ✅ | 🔥 | Discover services |
servicesList ⚡ | ✅ | ✅ | The current list of available services | |
onServicesReset 🌀 | ✅ | ✅ | The services changed & must be rediscovered | |
mtu 🌀 | ✅ | ✅ | Stream of current mtu value + changes | |
mtuNow ⚡ | ✅ | ✅ | The current mtu value | |
readRssi | ✅ | ✅ | 🔥 | Read RSSI from a connected device |
requestMtu | ✅ | 🔥 | Request to change the MTU for the device | |
requestConnectionPriority | ✅ | 🔥 | Request to update a high priority, low latency connection | |
bondState 🌀 | ✅ | Stream of device bond state. Can be useful on Android | ||
createBond | ✅ | 🔥 | Force a system pairing dialogue to show, if needed | |
removeBond | ✅ | 🔥 | Remove Bluetooth Bond of device | |
setPreferredPhy | ✅ | 🔥 | Set preferred RX and TX phy for connection and phy options | |
clearGattCache | ✅ | 🔥 | Clear android cache of service discovery results |
Android | iOS | Throws | Description | |
---|---|---|---|---|
uuid ⚡ | ✅ | ✅ | The uuid of characteristic | |
read | ✅ | ✅ | 🔥 | Retrieves the value of the characteristic |
write | ✅ | ✅ | 🔥 | Writes the value of the characteristic |
setNotifyValue | ✅ | ✅ | 🔥 | Sets notifications or indications on the characteristic |
isNotifying ⚡ | ✅ | ✅ | Are notifications or indications currently enabled | |
onValueReceived 🌀 | ✅ | ✅ | Stream of characteristic value updates received from the device | |
lastValue ⚡ | ✅ | ✅ | The most recent value of the characteristic | |
lastValueStream 🌀 | ✅ | ✅ | Stream of onValueReceived + writes |
Android | iOS | Throws | Description | |
---|---|---|---|---|
uuid ⚡ | ✅ | ✅ | The uuid of descriptor | |
read | ✅ | ✅ | 🔥 | Retrieves the value of the descriptor |
write | ✅ | ✅ | 🔥 | Writes the value of the descriptor |
onValueReceived 🌀 | ✅ | ✅ | Stream of descriptor value reads & writes | |
lastValue ⚡ | ✅ | ✅ | The most recent value of the descriptor | |
lastValueStream 🌀 | ✅ | ✅ | Stream of onValueReceived + writes |
The easiest way to debug issues in FlutterBluePlus is to make your own local copy.
cd /user/downloads
git clone https://github.com/boskokg/flutter_blue_plus.git
then in pubspec.yaml
add the repo by path:
flutter_blue_plus:
path: /user/downloads/flutter_blue_plus
Now you can edit the FlutterBluePlus code yourself.
Many common problems are easily solved.
1. try using another ble scanner app
Search the App Store for a BLE scanner apps and install it on your phone, and ideally other phones as well.
Can any of the phones find your ble device?
2. your device uses bluetooth classic, not BLE.
Headphones, speakers, keyboards, mice, gamepads, & printers all use Bluetooth Classic.
These devices may be found in System Settings, but they cannot be connected to by FlutterBluePlus. FlutterBluePlus only supports Bluetooth Low Energy.
3. your device stopped advertising.
- you might need to reboot your device
- you might need to put your device in "discovery mode"
- your phone may have already connected automatically
- another app may have already connected to your device
- another phone may have already connected to your device
Try looking through system devices:
// search system devices. i.e. any device connected to by *any* app
List<BluetoothDevice> system = await FlutterBluePlus.systemDevices;
for (var d in system) {
print('${r.device.platformName} already connected to! ${r.device.remoteId}');
if (d.platformName == "myBleDevice") {
await r.connect(); // must connect our app
}
}
4. your scan filters are wrong.
- try removing all scan filters
- for
withServices
to work, your device must actively advertise the serviceUUIDs it supports
1. Your ble device may be low battery
Bluetooth can become erratic when your peripheral device is low on battery.
2. Your ble device may have refused the connection or have a bug
Connection is a two-way process. Your ble device may be misconfigured.
3. You may be on the edge of the Bluetooth range.
The signal is too weak, or there are a lot of devices causing radio interference.
4. Some phones have an issue connecting while scanning.
The Huawei P8 Lite is one of the reported phones to have this issue. Try stopping your scanner before connecting.
5. Try restarting your phone
Bluetooth is a complicated system service, and can enter a bad state.
This is expected. There is no way to avoid it.
For privacy, iOS & macOS use a randomly generated uuid. This uuid will periodically change.
e.g. 6920a902-ba0e-4a13-a35f-6bc91161c517
Android uses the mac address of the bluetooth device. It never changes.
e.g. 05:A4:22:31:F7:ED
1. you are not calling the right function
lastValueStream
is called for await chr.read()
& await chr.write()
& await chr.setNotifyValue(true)
onValueReceived
is only called for await chr.read()
& await chr.setNotifyValue(true)
2. your device has nothing to send
If you are using await chr.setNotifyValue(true)
, your device chooses when to send data.
Try interacting with your device to get it to send new data.
3. your device has bugs
Try rebooting your ble device.
Some ble devices have buggy software and stop sending data
Verify that the mtu is large enough to hold your message.
device.mtu
If it still happens, it is a problem with your peripheral device.
You are probably forgetting to cancel the original stream.listen
resulting in multiple listens.
The easiest solution is to use device.cancelWhenDisconnected(subscription)
to cancel device subscriptions.
final subscription = characteristic.onValueReceived.listen((value) {
// ...
});
// make sure you have this line!
device.cancelWhenDisconnected(subscription);
await characteristic.setNotifyValue(true);
1. The characteristic is not writeable
Not all characteristics support write
.
Your device must have configured this characteristic to support write
.
2. The data length is too long
Characteristics only support writes up to a certain size.
writeWithoutResponse
: you can only write up to (MTU-3) at a time. This is a BLE limitation.
write (with response)
: look in the Usage section for functions you can use to solve this issue.
3. The characteristic does not support writeWithoutResponse
Not all characteristics support writeWithoutResponse
.
Your device must have configured this characteristic to support writeWithoutResponse
.
4. your bluetooth device turned off, or is out of range
If your device turns off mid-write, it will cause a failure.
5. Your Bluetooth device has bugs
Maybe your device crashed, or is not sending a response due to software bugs.
6. there is radio interference
Bluetooth is wireless and will not always work.
1. the characteristic is not readable
Not all characteristics support read
.
Your device must have configured this characteristic to support read
.
2. your bluetooth device turned off, or is out of range
If your device turns off mid-read, it will cause a failure.
3. Your Bluetooth device has bugs
Maybe your device crashed, or is not sending a response due to software bugs.
4. there is radio interference
Bluetooth is wireless and will not always work.
You need to wait for the bluetooth adapter to fully turn on.
await FlutterBluePlus.adapterState.where((state) => state == BluetoothAdapterState.on).first;
You can also use FlutterBluePlus.adapterState.listen(...)
. See Usage.
Added access to Bluetooth Hardware in the app's Xcode settings. See See Getting Started.
You can google this error. It is a common iOS ble error code.
It means your device stopped working. FlutterBluePlus cannot fix it.
There is no 100% solution.
FBP already has mitigations for this error, but Android will still fail with this code randomly.
The recommended solution is to catch
the error, and retry.
If you just added flutter_blue_plus to your pubspec.yaml, a hot reload / hot restart is not enough.
You need to fully stop your app and run again so that the native plugins are loaded.
Also try flutter clean
.