| FazBrowse GitHub Viewer | Trending | | Home |
| Tools: [Download Repo ZIP] [Original HTTPS Page] |
| Name | Name | Last commit date | ||
|---|---|---|---|---|
A cross-platform (Android/iOS/macOS/Windows/Linux/Web) Bluetooth Low Energy (BLE) plugin for Flutter.
Free: This package is free for commercial or personal use as long as you adhere to the BSD 3-Clause License.
Try it online, provided your browser supports Web Bluetooth.
| Android | iOS | macOS | Windows | Linux | Web | |
|---|---|---|---|---|---|---|
| startScan/stopScan | ✔️ | ✔️ | ✔️ | ✔️ | ✔️ | ✔️ |
| connect/disconnect | ✔️ | ✔️ | ✔️ | ✔️ | ✔️ | ✔️ |
| autoConnect | ✔️ | ✔️ | ✔️ | ❌ | ❌ | ❌ |
| getSystemDevices | ✔️ | ✔️ | ✔️ | ✔️ | ✔️ | ❌ |
| discoverServices | ✔️ | ✔️ | ✔️ | ✔️ | ✔️ | ✔️ |
| read | ✔️ | ✔️ | ✔️ | ✔️ | ✔️ | ✔️ |
| write | ✔️ | ✔️ | ✔️ | ✔️ | ✔️ | ✔️ |
| subscriptions | ✔️ | ✔️ | ✔️ | ✔️ | ✔️ | ✔️ |
| pair | ✔️ | ✔️ | ✔️ | ✔️ | ✔️ | ⏺ |
| unpair | ✔️ | ❌ | ❌ | ✔️ | ✔️ | ❌ |
| isPaired | ✔️ | ✔️ | ✔️ | ✔️ | ✔️ | ✔️ |
| onPairingStateChange | ✔️ | ⏺ | ⏺ | ✔️ | ✔️ | ⏺ |
| getBluetoothAvailabilityState | ✔️ | ✔️ | ✔️ | ✔️ | ✔️ | ❌ |
| enable/disable Bluetooth | ✔️ | ❌ | ❌ | ✔️ | ✔️ | ❌ |
| onAvailabilityChange | ✔️ | ✔️ | ✔️ | ✔️ | ✔️ | ✔️ |
| requestMtu | ✔️ | ✔️ | ✔️ | ✔️ | ✔️ | ❌ |
| requestConnectionPriority | ✔️ | ❌ | ❌ | ❌ | ❌ | ❌ |
| onConnectionParametersChange | ✔️ | ❌ | ❌ | ❌ | ❌ | ❌ |
| readRssi | ✔️ | ✔️ | ✔️ | ❌ | 🚧 | ❌ |
| requestPermissions | ✔️ | ✔️ | ✔️ | ✔️ | ✔️ | ✔️ |
| API | Android | iOS | macOS | Windows | Linux | Web |
|---|---|---|---|---|---|---|
| getCapabilities | ✔️ | ✔️ | ✔️ | ✔️ | 🚧 | ❌ |
| getAvailabilityState* | ✔️ | ✔️ | ✔️ | ✔️ | 🚧 | ❌ |
| getAdvertisingState | ✔️ | ✔️ | ✔️ | ✔️ | 🚧 | ❌ |
| addService | ✔️ | ✔️ | ✔️ | ✔️ | 🚧 | ❌ |
| removeService | ✔️ | ✔️ | ✔️ | ✔️ | 🚧 | ❌ |
| clearServices | ✔️ | ✔️ | ✔️ | ✔️ | 🚧 | ❌ |
| getServices | ✔️ | ✔️ | ✔️ | ✔️ | 🚧 | ❌ |
| startAdvertising | ✔️ | ✔️ | ✔️ | ✔️ | 🚧 | ❌ |
| stopAdvertising | ✔️ | ✔️ | ✔️ | ✔️ | 🚧 | ❌ |
| updateCharacteristicValue** | ✔️ | ✔️ | ✔️ | ✔️ | 🚧 | ❌ |
| getSubscribedClients | ✔️ | ✔️ | ✔️ | ✔️ | 🚧 | ❌ |
| getMaximumNotifyLength | ✔️ | ✔️ | ✔️ | ✔️ | 🚧 | ❌ |
| event streams*** | ✔️ | ✔️ | ✔️ | ✔️ | 🚧 | ❌ |
* getAvailabilityState returns a snapshot. Listen to advertisingStateStream, connectionStateStream, and related streams for runtime updates. ** Pass deviceId to notify one client; omit it to notify all subscribed clients (when supported — see getCapabilities().supportsTargetedCharacteristicUpdate). *** advertisingStateStream, characteristicSubscriptionStream, connectionStateStream, serviceAddedStream, mtuChangedStream.
Add universal_ble in your pubspec.yaml:
dependencies:
universal_ble:and import it wherever you want to use it:
import 'dart:typed_data';
import 'package:universal_ble/universal_ble.dart';Important: Before using BLE features, make sure to check the Permissions section to see what setup is needed for your target platform (Android, iOS, macOS, Windows, Linux, or Web).
The very first thing you need to do before being able to connect to a device is to discover it by calling startScan();
// Get scan updates from stream
UniversalBle.scanStream.listen((BleDevice bleDevice) {
// e.g. Use BleDevice ID to connect
});
// Or set a handler
UniversalBle.onScanResult = (bleDevice) {}
// Perform a scan
UniversalBle.startScan();
// Or optionally add a scan filter
UniversalBle.startScan(
scanFilter: ScanFilter(
withServices: ["SERVICE_UUID"],
withManufacturerData: [ManufacturerDataFilter(companyIdentifier: 0x004c)],
withNamePrefix: ["NAME_PREFIX"],
)
);
// Stop scanning
UniversalBle.stopScan();
// Check if scanning
UniversalBle.isScanning();Before initiating a scan, ensure that Bluetooth is available:
AvailabilityState state = await UniversalBle.getBluetoothAvailabilityState();
// Start scan only if Bluetooth is powered on
if (state == AvailabilityState.poweredOn) {
UniversalBle.startScan();
}
// Listen to bluetooth availability changes using stream
UniversalBle.availabilityStream.listen((state) {
if (state == AvailabilityState.poweredOn) {
UniversalBle.startScan();
}
});
// Or set a handler
UniversalBle.onAvailabilityChange = (state) {};See the Bluetooth Availability section for more.
Already connected devices, connected either through previous sessions, other apps or through system settings, won't show up as scan results. You can get those using getSystemDevices().
// Get already connected devices.
// You can set `withServices` to narrow down the results.
// On `Apple`, `withServices` is required to get any connected devices. If not passed, several [18XX] generic services will be set by default.
List<BleDevice> devices = await UniversalBle.getSystemDevices(withServices: []);For each such device the isSystemDevice property will be true.
You still need to explicitly connect to them before being able to use them.
You can optionally set a filter when scanning. A filter can have multiple conditions (services, manufacturerData, namePrefix) and all conditions are in OR relation, returning results that match any of the given conditions.
When setting this parameter, the scan results will only include devices that advertise any of the specified services.
List<String> withServices;Note: On web you have to specify services before you are able to use them. See the web section for more details.
Use the withManufacturerData parameter to filter devices by manufacturer data. When you pass a list of ManufacturerDataFilter objects to this parameter, the scan results will only include devices that contain any of the specified manufacturer data.
You can filter manufacturer data by company identifier, payload prefix, or payload mask.
List<ManufacturerDataFilter> withManufacturerData = [ManufacturerDataFilter(
companyIdentifier: 0x004c,
payloadPrefix: Uint8List.fromList([0x001D,0x001A]),
payloadMask: Uint8List.fromList([1,0,1,1]))
];Use the withNamePrefix parameter to filter devices by names (case sensitive). When you pass a list of names, the scan results will only include devices that have this name or start with the provided parameter.
List<String> withNamePrefix;Use exclusion filters to exclude specific devices from scan results:
exclusionFilters: [
ExclusionFilter(
namePrefix: 'EXCLUDED_NAME',
services: ['EXCLUDED_SERVICE_UUID'],
manufacturerDataFilter: [ManufacturerDataFilter(companyIdentifier: 0x004c)],
),
]Connects to the BLE device. This method initiates a connection to the Bluetooth device.
await bleDevice.connect();Disconnects from the BLE device. This method terminates the connection to the Bluetooth device.
await bleDevice.disconnect();bleDevice.connectionStream.listen((isConnected) {
debugPrint('Is device connected?: $isConnected');
});bool isConnected = await bleDevice.isConnected;// Can be connected, disconnected, connecting or disconnecting
BleConnectionState connectionState = await bleDevice.connectionState;You can enable automatic reconnection by setting the autoConnect parameter to true. When enabled, the system will automatically attempt to reconnect to the device when it becomes available again.
await bleDevice.connect(autoConnect: true);After establishing a connection, services need to be discovered. This method will discover all services and their characteristics.
If you don't call this method then it will be automatically called when you try to get any service or characteristic.
Discovers the services offered by the device. Returns a Future<List<BleService>>. After discovery services are cached and each call of this method updates the cache.
List<BleService> services = await bleDevice.discoverServices();
for (var service in services) {
debugPrint('Service UUID: ${service.uuid}');
}Retrieves a specific service. Returns a Future<BleService>.
BleService service = await bleDevice.getService('180a');Retrieves a specific characteristic from a service. Returns a Future<BleCharacteristic>.
BleCharacteristic characteristic = await bleDevice.getCharacteristic('180a','2a56');Or retrieve from BleService
BleCharacteristic characteristic = await service.getCharacteristic('2a56');You need to first discover services before you are able to read and write to characteristics.
Uint8List value = await characteristic.read();await characteristic.write([0x01, 0x02, 0x03]);
await characteristic.write([0x01, 0x02, 0x03], withResponse: false);Get BleCharacteristic using bleDevice.getCharacteristic
A stream of Uint8List that emits values received from the characteristic. Listen to this stream to receive updates whenever the characteristic's value changes.
characteristic.onValueReceived.listen((value) {
debugPrint('Received value: ${value.toString()}');
});Subscribe to notifications for this characteristic. Throws an exception if the characteristic does not support notifications.
await characteristic.notifications.subscribe();Subscribe to indications for this characteristic. Throws an exception if the characteristic does not support indications.
await characteristic.indications.subscribe();Unsubscribe from notifications and indications of this characteristic.
await characteristic.unsubscribe();await bleDevice.pair();For Apple and Web, pairing support depends on the device. Pairing is triggered automatically by the OS when you try to read/write from/to an encrypted characteristic.
Calling bleDevice.pair() will only trigger pairing if the device has an encrypted read characteristic.
If your device only has encrypted write characteristics or you happen to know which encrypted read characteristic you want to use, you can pass it with a pairingCommand.
await bleDevice.pair(pairingCommand: BleCommand(service:"SERVICE", characteristic:"ENCRYPTED_CHARACTERISTIC"));After pairing you can check the pairing status.
// Check current pairing state
bool? isPaired = bleDevice.isPaired();For Apple and Web, you have to pass a "pairingCommand" with an encrypted read or write characteristic. If you don't pass it then it will return null.
bool? isPaired = await bleDevice.isPaired(pairingCommand: BleCommand(service:"SERVICE", characteristic:"ENCRYPTED_CHARACTERISTIC"));To discover encrypted characteristics, make sure your device is not paired and use the example app to read/write to all discovered characteristics one by one. If one of them triggers pairing, that means it is encrypted and you can use it to construct BleCommand(service:"SERVICE", characteristic:"ENCRYPTED_CHARACTERISTIC").
// Get pairing state updates using stream
bleDevice.pairingStateStream.listen((bool paired) {
// Handle pairing state change
});bleDevice.unpair();// Get current Bluetooth availability state
AvailabilityState availabilityState = UniversalBle.getBluetoothAvailabilityState(); // e.g. poweredOff or poweredOn,
// Receive Bluetooth availability changes
UniversalBle.onAvailabilityChange = (state) {
// Handle the new Bluetooth availability state
};
// Enable Bluetooth programmatically
UniversalBle.enableBluetooth();
// Disable Bluetooth programmatically
UniversalBle.disableBluetooth();int mtu = await bleDevice.requestMtu(256);⚠️ Note: Requesting an MTU is a best-effort operation. On many platforms the final MTU is fully controlled by the OS and remote device.
MTU negotiation is largely platform- and stack-managed, and often cannot be explicitly controlled by applications:
iOS / macOS
Android
Windows
Linux (BlueZ)
Web
When developing cross-platform BLE applications and devices:
On Android, you can request a connection parameter update to tune the BLE connection interval. This can yield a 3–7× throughput improvement for data-intensive transfers.
// Before starting high-throughput data transfer:
await UniversalBle.requestConnectionPriority(
deviceId,
BleConnectionPriority.highPerformance,
);Note: Only supported on Android. On all other platforms this throws UniversalBleException with code notSupported. Call this after connecting and after requestMtu(), before beginning data transfer.
The OS may later change connection parameters without your app requesting it (e.g. for power saving), which can reduce throughput. On Android API 26+, set UniversalBle.onConnectionParametersChange and react if needed:
UniversalBle.onConnectionParametersChange = (update) {
if (update.deviceId != deviceId || !update.isSuccess) return;
// Prefer intervalMs for throughput decisions; estimatedPriority is approximate.
if (update.intervalMs > 50) {
UniversalBle.requestConnectionPriority(
deviceId,
BleConnectionPriority.highPerformance,
);
}
};Note: Re-requesting high priority on every update can fight the OS power manager — debounce in app code. Requires Android API 26+ (BleCapabilities.supportsConnectionParametersUpdates).
Read the signal strength (RSSI) of a connected device.
int rssi = await bleDevice.readRssi();⚠️ Note: The device must be connected before reading RSSI.
Android / iOS / macOS: Fully supported.
Windows / Linux / Web: Not supported.
By default, all commands are executed in a global queue (QueueType.global), with each command waiting for the previous one to finish. While this method is slower it is the safest to avoid command exceptions and therefore is the default.
If you want to parallelize commands between multiple devices, you can set:
// Create a separate queue for each device.
UniversalBle.queueType = QueueType.perDevice;You can have separate queues by passing an optional queueId. Commands with the same queueId are serialized together, but run in parallel with both QueueType.perDevice and QueueType.global:
UniversalBle.write(deviceId, service, char, value1, queueId: '1');
UniversalBle.write(deviceId, service, char, value2, queueId: '2');You can also completely disable the queue and batch all commands, even for the same device, by using:
// Disable queue
UniversalBle.queueType = QueueType.none;Keep in mind that some platforms (e.g. Android) may not handle well devices that fail to process consecutive commands without a minimum interval. Therefore, it is not advised to set queueType to none.
You can get queue updates by setting:
// Get queue state updates
UniversalBle.onQueueUpdate = (String id, int remainingItems) {
debugPrint("Queue: $id Remaining: $remainingItems");
};To clear a queue:
// Clear global queue
UniversalBle.clearQueue(BleCommandQueue.globalQueueId);
// Clear a per-device queue (when queueType is perDevice)
UniversalBle.clearQueue(deviceId);
// Clear a custom queue (same string passed as queueId to read/write/etc.)
UniversalBle.clearQueue('customQueueId');
// Clear all queues
UniversalBle.clearQueue();By default, all commands have a global timeout of 10 seconds.
// Change timeout
UniversalBle.timeout = const Duration(seconds: 10);
// Disable timeout
UniversalBle.timeout = null;You can also specify the timeout parameter when sending a command. This will override the global timeout.
Universal BLE provides a unified and type-safe error handling system across all platforms. All errors are represented using the UniversalBleException base class with typed error codes from the UniversalBleErrorCode enum.
All errors are categorized using the UniversalBleErrorCode enum, which includes codes for:
try {
await bleDevice.connect();
} on ConnectionException catch (e) {
// Handle connection-specific errors
switch (e.code) {
case UniversalBleErrorCode.connectionTimeout:
// Handle timeout
break;
case UniversalBleErrorCode.connectionFailed:
// Handle connection failure
break;
case UniversalBleErrorCode.deviceDisconnected:
// Handle disconnection
break;
default:
// Handle other connection errors
}
} on UniversalBleException catch (e) {
// Handle other BLE errors
print('Error code: ${e.code}, Message: ${e.message}');
}The error parser automatically converts platform-specific error formats (strings, numeric codes, PlatformExceptions) into the unified UniversalBleErrorCode enum, ensuring consistent error handling across all platforms.
universal_ble provides peripheral mode through UniversalBlePeripheral, so your app can advertise as a peripheral "server" in addition to client mode.
import 'package:universal_ble/universal_ble.dart';
final caps = await UniversalBlePeripheral.getCapabilities();
if (!caps.supportsPeripheralMode) return;
final readiness = await UniversalBlePeripheral.getAvailabilityState();
if (readiness != PeripheralReadinessState.ready) return;Peripheral GATT services use BlePeripheralService, BlePeripheralCharacteristic, and BlePeripheralDescriptor. Each characteristic requires permissions; descriptors can include an initial value (for example HID Report Reference 0x2908).
import 'package:universal_ble/universal_ble.dart';
const batteryService = '0000180f-0000-1000-8000-00805f9b34fb';
const batteryLevelChar = '00002a19-0000-1000-8000-00805f9b34fb';
const heartRateService = '0000180d-0000-1000-8000-00805f9b34fb';
const heartRateChar = '00002a37-0000-1000-8000-00805f9b34fb';
await UniversalBlePeripheral.addService(
BlePeripheralService(
uuid: batteryService,
primary: true,
characteristics: [
BlePeripheralCharacteristic(
uuid: batteryLevelChar,
properties: [
CharacteristicProperty.read,
CharacteristicProperty.notify,
],
permissions: [
PeripheralAttributePermission.readable,
PeripheralAttributePermission.writeable,
],
descriptors: [
BlePeripheralDescriptor(uuid: '00002902-0000-1000-8000-00805f9b34fb'),
],
),
],
),
);
await UniversalBlePeripheral.addService(
BlePeripheralService(
uuid: heartRateService,
characteristics: [
BlePeripheralCharacteristic(
uuid: heartRateChar,
properties: [
CharacteristicProperty.read,
CharacteristicProperty.notify,
CharacteristicProperty.write,
],
permissions: [
PeripheralAttributePermission.readable,
PeripheralAttributePermission.writeable,
],
),
],
),
);
final services = await UniversalBlePeripheral.getServices();
await UniversalBlePeripheral.removeService(heartRateService);
await UniversalBlePeripheral.clearServices();On Android, passing localName may temporarily change the system Bluetooth device name (so it can appear in the advertisement). The plugin restores the previous name when advertising stops, if starting advertising fails, or when the plugin is disposed.
On Windows, GattServiceProvider-based advertising does not support localName, manufacturer data, or a scan-response flag; pass null for those parameters or the call returns a not-supported error. Use getCapabilities() to check feature support before calling.
import 'dart:typed_data';
import 'package:flutter/foundation.dart';
import 'package:universal_ble/universal_ble.dart';
// Uses batteryService from Service Management above.
final isWindows = !kIsWeb && defaultTargetPlatform == TargetPlatform.windows;
final caps = await UniversalBlePeripheral.getCapabilities();
await UniversalBlePeripheral.startAdvertising(
services: [batteryService],
localName: isWindows ? null : 'UniversalBlePeripheral',
manufacturerData: isWindows || !caps.supportsManufacturerDataInAdvertisement
? null
: ManufacturerData(
0x012d,
Uint8List.fromList([0x03, 0x00, 0x64, 0x00]),
),
platformConfig: PeripheralPlatformConfig(
android: PeripheralAndroidOptions(
addManufacturerDataInScanResponse: false,
),
),
);
final advertisingState = await UniversalBlePeripheral.getAdvertisingState();
if (advertisingState == PeripheralAdvertisingState.advertising) {
// Peripheral is advertising.
}
await UniversalBlePeripheral.stopAdvertising();Register read/write handlers separately. Return null to let the stack use the characteristic's current value.
import 'dart:typed_data';
import 'package:universal_ble/universal_ble.dart';
UniversalBlePeripheral.setReadRequestHandlers(
(deviceId, characteristicId, offset, value) {
return PeripheralReadRequestResult(
value: value ?? Uint8List(0),
);
},
);
UniversalBlePeripheral.setWriteRequestHandlers(
(deviceId, characteristicId, offset, value) {
return PeripheralWriteRequestResult();
},
);
UniversalBlePeripheral.setDescriptorReadRequestHandlers(
(deviceId, characteristicId, descriptorId, offset, value) {
return PeripheralReadRequestResult(
value: value ?? Uint8List(0),
);
},
);
UniversalBlePeripheral.setDescriptorWriteRequestHandlers(
(deviceId, characteristicId, descriptorId, offset, value) {
return PeripheralWriteRequestResult();
},
);import 'dart:typed_data';
import 'package:universal_ble/universal_ble.dart';
await UniversalBlePeripheral.updateCharacteristicValue(
characteristicId: batteryLevelChar,
value: Uint8List.fromList([92]),
);
// Notify one client (when getCapabilities().supportsTargetedCharacteristicUpdate).
await UniversalBlePeripheral.updateCharacteristicValue(
characteristicId: batteryLevelChar,
value: Uint8List.fromList([88]),
deviceId: deviceId,
);Useful to restore in-app state after a process restart (subscription callbacks are not replayed).
import 'package:universal_ble/universal_ble.dart';
final subscribers = await UniversalBlePeripheral.getSubscribedClients(
batteryLevelChar,
);
for (final deviceId in subscribers) {
final maxNotifyLength =
await UniversalBlePeripheral.getMaximumNotifyLength(deviceId);
// maxNotifyLength is null when unknown for this device.
}import 'package:universal_ble/universal_ble.dart';
UniversalBlePeripheral.advertisingStateStream.listen(
(BlePeripheralAdvertisingStateChanged event) {
// event.state, event.error
},
);
UniversalBlePeripheral.characteristicSubscriptionStream.listen(
(BlePeripheralCharacteristicSubscriptionChanged event) {
// event.deviceId, event.characteristicId, event.isSubscribed, event.name
},
);
UniversalBlePeripheral.connectionStateStream.listen(
(BlePeripheralConnectionStateChanged event) {
// event.deviceId, event.connected
},
);
UniversalBlePeripheral.serviceAddedStream.listen(
(BlePeripheralServiceAdded event) {
// event.serviceId, event.error
},
);
UniversalBlePeripheral.mtuChangedStream.listen(
(BlePeripheralMtuChanged event) {
// event.deviceId, event.mtu
},
);Universal BLE is agnostic to the UUID format of services and characteristics regardless of the platform the app runs on. When passing a UUID, you can pass it in any format (long/short) or character case (upper/lower case) you want. Universal BLE will take care of necessary conversions, across all platforms, so that you don't need to worry about underlying platform differences.
For consistency, all characteristic and service UUIDs will be returned in lowercase 128-bit format, across all platforms, e.g. 0000180a-0000-1000-8000-00805f9b34fb.
If you need to convert any UUIDs in your app you can use the following methods.
BleUuidParser.string("180A"); // "0000180a-0000-1000-8000-00805f9b34fb"
BleUuidParser.string("0000180A-0000-1000-8000-00805F9B34FB"); // "0000180a-0000-1000-8000-00805f9b34fb"BleUuidParser.number(0x180A); // "0000180a-0000-1000-8000-00805f9b34fb"BleUuidParser.compare("180a","0000180A-0000-1000-8000-00805F9B34FB"); // trueYou need to perform the following setups:
Add the following permissions to your AndroidManifest.xml file:
<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_COARSE_LOCATION" android:maxSdkVersion="28" />
<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" android:maxSdkVersion="30" />
<uses-permission android:name="android.permission.BLUETOOTH_SCAN" android:usesPermissionFlags="neverForLocation" />If your app uses iBeacons or BLUETOOTH_SCAN to determine location, change the last 2 permissions to:
<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" />
<uses-permission android:name="android.permission.BLUETOOTH_SCAN" />The withAndroidFineLocation parameter in requestPermissions() controls location permission requests on Android:
If your app uses peripheral advertising, add:
<uses-permission android:name="android.permission.BLUETOOTH_ADVERTISE" />By default, BLE 5 extended advertisements are scanned (API 26+, unchanged from prior releases). Set legacy: true for legacy BLE 4.x devices (e.g. ESP32).
UniversalBle.startScan(
platformConfig: PlatformConfig(
android: AndroidOptions(
legacy: true, // omit for extended BLE 5 (default)
scanMode: AndroidScanMode.lowLatency,
callbackType: [AndroidScanCallbackType.allMatches],
requestLocationPermission: false,
),
),
);Universal BLE supports BLE scanning from background services (e.g., using flutter_foreground_task or similar packages) on Android. When running in a background context without an Activity:
Best Practice: Request permissions while your app is in the foreground before starting any background BLE operations:
// Request permissions in foreground (e.g., during app setup)
await UniversalBle.requestPermissions();
// Later, in your ForegroundTask, scanning will work if permissions were granted
await UniversalBle.startScan();For Bluetooth usage (including peripheral mode), add both keys to your app's Info.plist:
Example:
<key>NSBluetoothAlwaysUsageDescription</key>
<string>This app uses Bluetooth to scan, connect, and advertise to nearby devices.</string>
<key>NSBluetoothPeripheralUsageDescription</key>
<string>This app uses Bluetooth to advertise services to nearby devices.</string>Use clear, user-facing text that explains why Bluetooth is needed in your app.
Add the Bluetooth capability to the macOS app from Xcode.
Permissions are automatically requested when calling startScan(). You can also manually call requestPermissions() if needed.
On iOS, when your app declares the bluetooth-central background mode and Bluetooth permission is already granted, the central manager is created at launch with a CBCentralManagerOptionRestoreIdentifierKey, so CoreBluetooth can relaunch your app in the background when a connected peripheral has activity, and hand the live connection back to the plugin. If permission has not been granted yet, creation is deferred until a central BLE API (such as startScan() or connect()) is called.
To opt in, declare the Uses Bluetooth LE accessories background mode. After enabling it, in Info.plist you should have:
<key>UIBackgroundModes</key>
<array>
...
<string>bluetooth-central</string>
...
</array>Notes:
Your Bluetooth adapter needs to support at least Bluetooth 4.0. If you have more than 1 adapters, the first one returned from the system will be picked.
When publishing on Windows, you need to declare the following capabilities: bluetooth, radios.
Your Bluetooth adapter needs to support at least Bluetooth 4.0. If you have more than 1 adapters, the first one returned from the system will be picked.
When publishing on Linux as a snap, you need to declare the bluez plug in snapcraft.yaml.
...
plugs:
- bluez
On web, the withServices parameter in the ScanFilter is used as optional_services as well as a services filter. You have to set this parameter to ensure that you can access the specified services after connecting to the device. You can leave it empty for the rest of the platforms if your device does not advertise services.
ScanFilter(
withServices: kIsWeb ? ["SERVICE_UUID"] : [],
)If you don't want to apply any filter for these services but still want to access them, after connection, use PlatformConfig.
UniversalBle.startScan(
platformConfig: PlatformConfig(
web: WebOptions(
optionalServices: ["SERVICE_UUID"]
)
)
)No runtime permissions are required. The requestPermissions() method always succeeds on Web.
Calling requestPermissions() is optional. Permissions are automatically requested when calling startScan(). However, you can manually call requestPermissions() if you want to:
The requestPermissions() method:
// Optional: Manually request permissions
UniversalBle.requestPermissions(
withAndroidFineLocation: false,
);Note: When calling startScan(), permissions are automatically requested. To configure location permission requests during scanning, use requestLocationPermission on AndroidOptions (see Android scan options):
UniversalBle.startScan(
platformConfig: PlatformConfig(
android: AndroidOptions(
requestLocationPermission: false,
),
),
);No runtime permissions are required. The requestPermissions() method always succeeds on Windows and Linux platforms.
// Create a class that extends UniversalBlePlatform
class UniversalBleMock extends UniversalBlePlatform {
// Implement all commands
}
UniversalBle.setInstance(UniversalBleMock());Configure logging to help debug Ble operations
Set the log level during app initialization, default level is none
void main() async {
WidgetsFlutterBinding.ensureInitialized();
// Enable verbose logging to see all BLE operations
await UniversalBle.setLogLevel(BleLogLevel.verbose);
runApp(MyApp());
}During Flutter hot restart in debug mode, the app state is reset but native Bluetooth connections and scan operations may persist. This can lead to connection issues or stale state.
Use the following helper function to properly clean up BLE state before your app restarts.void main() async {
WidgetsFlutterBinding.ensureInitialized();
// Reset BLE state before app initialization
await resetBleState();
runApp(MyApp());
}
/// Resets BLE state by stopping scans and disconnecting all devices.
/// Make sure you have Bluetooth permissions before calling this function.
Future<void> resetBleState() async {
// Skip reset in release mode or on web
if (!kDebugMode || kIsWeb) return;
// Check Bluetooth availability
AvailabilityState availabilityState =
await UniversalBle.getBluetoothAvailabilityState();
// Skip if Bluetooth is not powered on
if (availabilityState != AvailabilityState.poweredOn) {
debugPrint('Reset: Bluetooth is not powered on');
return;
}
// Stop scanning
if (await UniversalBle.isScanning()) {
debugPrint('Reset: Stopping scan');
await UniversalBle.stopScan();
}
// Disconnect all connected devices
List<String> withServices = [];
// On Apple platforms, you must specify services to discover connected devices
if (defaultTargetPlatform == TargetPlatform.macOS ||
defaultTargetPlatform == TargetPlatform.iOS) {
// Replace with your known device service UUIDs
withServices = ["0x180A"];
}
List<BleDevice> connectedDevices =
await UniversalBle.getSystemDevices(withServices: withServices);
for (var device in connectedDevices) {
debugPrint('Reset: Disconnecting device: ${device.deviceId}');
await UniversalBle.disconnect(device.deviceId);
}
debugPrint('Reset: Done');
}This repo includes an example app with two tabs:
For a full-blown app, check Universal-BLE.
For more granular control, you can use the Low-Level API. This API is "Device ID"-based, offering greater flexibility by enabling direct calls without the need for object instances.
Here are some of the apps leveraging the power of universal_ble:
💡 Built something cool with Universal BLE?
We'd love to showcase your app here!
Open a pull request and add it to this section.
| Back | FazBrowse Home | New Git URL |