quick_blue is a federated Flutter plugin for Bluetooth Low Energy on Android,
iOS, macOS, Windows, and Linux.
The repository is a Dart workspace:
quick_blue/: app-facing plugin package and Android implementationquick_blue_darwin/: iOS and macOS implementationquick_blue_linux/: Linux implementation using BlueZquick_blue_windows/: Windows implementationquick_blue_platform_interface/: shared APIs, models, and testsquick_blue/example/: BLE explorer example app and hardware smoke tests
This package is not published on pub.dev yet. Add it from your chosen Git source or local checkout:
dependencies:
quick_blue:
git:
url: <repository-url>
path: quick_blueThen import it:
import 'package:quick_blue/quick_blue.dart';Configure the Bluetooth permissions required by each target platform. The example app includes working platform manifests and plist entries.
| API | Android | iOS | macOS | Windows | Linux |
|---|---|---|---|---|---|
isBluetoothAvailable |
yes | yes | yes | yes | yes |
bluetoothStateStream |
yes | yes | yes | yes | yes |
scan / scanResults |
yes | yes | yes | yes | yes |
connectedDevices |
yes | yes[1] | yes[1] | yes | yes |
connect / disconnect |
yes | yes | yes | yes | yes |
bondState / pair |
yes | no[2] | no[2] | no | yes |
discoverServices |
yes | yes | yes | yes | yes |
readValue / writeValue |
yes | yes | yes | yes | yes |
setNotifiable |
yes | yes | yes | yes | yes |
requestMtu |
yes | yes | yes | yes | yes |
bluetoothStateStream emits the latest available Bluetooth state first for each
listener. Android, iOS, macOS, and Linux then emit live state changes; Windows
currently emits only the current availability snapshot.
[1] iOS and macOS use CoreBluetooth's connected-peripheral lookup, which requires service UUIDs to find system-connected peripherals.
[2] iOS and macOS do not expose app-initiated BLE pairing. CoreBluetooth prompts automatically when an encrypted characteristic requires pairing.
Enable CoreBluetooth state preservation/restoration on iOS and macOS before any other Bluetooth call:
await QuickBlue.configure(maintainState: true);On iOS, apps that rely on restoration after background termination also need the
bluetooth-central background mode in UIBackgroundModes.
Scan for nearby peripherals:
final scanSubscription = QuickBlue.scanResults().listen((result) {
print('${result.deviceId} ${result.name} RSSI=${result.rssi}');
});
// Stop scanning when the UI no longer needs results.
await scanSubscription.cancel();Use common filters and scan options when they fit your scanner behavior:
final scanSubscription = QuickBlue.scanResults(
scanFilter: ScanFilter(serviceUuids: ['180d'], rssi: -80),
scanOptions: const ScanOptions(
allowDuplicates: false,
scanMode: ScanMode.balanced,
),
).listen((result) {
print('${result.deviceId} ${result.name} RSSI=${result.rssi}');
});Platform-specific scan options are also available when you need native scanner controls such as Android PHY, CoreBluetooth solicited services, BlueZ pathloss, or Windows signal-strength timing.
Use QuickBlue.scan() when you only need BluetoothDevice handles. Use
QuickBlue.scanResults() when you need advertisement fields such as RSSI,
service UUIDs, service data, or manufacturer data. BluetoothDevice and
BluetoothCharacteristic are lightweight handles: creating one does not start
platform work until you call an operation on it.
Get device handles for peripherals that are already connected:
final devices = await QuickBlue.connectedDevices(
serviceUuids: ['0000180d-0000-1000-8000-00805f9b34fb'],
);Pass service UUIDs when targeting iOS or macOS; CoreBluetooth only returns connected peripherals that match the supplied services.
Connect, discover services, and interact with a characteristic:
import 'dart:typed_data';
import 'package:quick_blue/quick_blue.dart';
Future<void> readWriteNotify({
required String deviceId,
required String serviceId,
required String characteristicId,
}) async {
final device = QuickBlue.device(deviceId);
await device.connect().timeout(const Duration(seconds: 15));
try {
final services = await device.discoverServices();
for (final service in services) {
print('${service.uuid}: ${service.characteristics}');
}
final characteristic = device.characteristic(serviceId, characteristicId);
final notifications = characteristic.notifications().listen((value) {
print('notification: $value');
});
final currentValue = await characteristic.read();
print('read: $currentValue');
await characteristic.write(
Uint8List.fromList([0x01]),
BleOutputProperty.withResponse,
);
await notifications.cancel();
} finally {
await device.disconnect().timeout(const Duration(seconds: 5));
}
}Pair with a device on platforms that expose app-initiated bonding:
final device = QuickBlue.device(deviceId);
final state = await device.bondState();
if (state != BluetoothBondState.bonded) {
await device.pair();
}The static connect, disconnect, discoverServices, readValue,
writeValue, and setNotifiable methods delegate through the same handle API.
Prefer keeping a BluetoothDevice when doing more than one operation.
When you know a characteristic UUID but not its service UUID, discover a
BluetoothGatt snapshot and resolve a service-scoped characteristic handle:
final gatt = await device.discoverGatt();
final characteristic = gatt.characteristic(characteristicId);
final value = await characteristic.read();Use gatt.hasCharacteristic(characteristicId, service: serviceId) when you only
need to check whether a discovered GATT view contains a characteristic.
If the same characteristic UUID appears under multiple services, pass the service UUID to disambiguate:
final characteristic = gatt.characteristic(
characteristicId,
service: serviceId,
);Use characteristic.notifications() when a subscription should own notification
setup and teardown. Use characteristic.valueStream with
characteristic.setNotifiable(...) when callers need to subscribe before
enabling notifications or manage notification lifetime separately.
ScanFilter.rssi and common ScanOptions fields are applied consistently by
the Dart lifecycle APIs and mapped to native filters where the platform supports
them. Omitted common options preserve Quick Blue's existing platform defaults.
- Android companion-device association is available through
QuickBlue.companion. UseisSupported()before showing Android-only association UI, then callassociate(),associations(), anddisassociate(). The older static companion methods remain as deprecated compatibility wrappers. - Android and Linux expose app-initiated BLE pairing through
BluetoothDevice.pair(). Android shows the system pairing flow and the returned future completes when bonding succeeds or fails. - iOS and macOS use CoreBluetooth.
requestMtureturns the negotiated MTU currently in effect; CoreBluetooth does not let apps request an exact MTU or manually start BLE pairing. - Linux requires BlueZ.
- Windows has platform-specific service discovery behavior. App-initiated pairing is not currently implemented by this plugin.
Set up dependencies from the repository root:
flutter pub getRun the common checks:
dart format .
flutter analyzeRun package-focused tests:
cd quick_blue_platform_interface && flutter test
cd quick_blue_darwin && flutter test
cd quick_blue/example && flutter testRun hardware-backed integration tests from the example app when changing scan, connect, service discovery, or device-switching behavior:
cd quick_blue/example
QUICK_BLUE_HIDE_TEST_WINDOW=1 \
flutter test integration_test/ble_smoke_test.dart -d macos
QUICK_BLUE_HIDE_TEST_WINDOW=1 \
flutter test integration_test/ble_ui_switch_test.dart -d macosThese tests need Bluetooth permission, powered-on Bluetooth hardware, and
nearby BLE advertisements. ble_smoke_test.dart targets all quick_blue
platforms and includes read coverage plus opt-in write coverage for known
test peripherals. ble_ui_switch_test.dart targets macOS and Linux. See
quick_blue/example/README.md for optional
Dart defines that target specific devices.
The example app also includes a hardware-backed characteristic benchmark for high-volume notification throughput and read latency:
cd quick_blue/example
QUICK_BLUE_HIDE_TEST_WINDOW=1 \
flutter test integration_test/ble_characteristic_benchmark_test.dart -d macos \
--dart-define=QUICK_BLUE_BENCHMARK_DEVICE_ID='DEVICE_ID' \
--dart-define=QUICK_BLUE_BENCHMARK_NOTIFY_SERVICE_UUID='SERVICE_UUID' \
--dart-define=QUICK_BLUE_BENCHMARK_NOTIFY_CHARACTERISTIC_UUID='CHARACTERISTIC_UUID'Run the Windows smoke test in a Dockur Windows VM from the repository root:
QUICK_BLUE_WINDOWS_USB_VENDOR_ID=0x0a12 \
QUICK_BLUE_WINDOWS_USB_PRODUCT_ID=0x0001 \
scripts/windows-integration-test.shThe script starts dockurr/windows, mounts this checkout into the guest, and
runs quick_blue/example/integration_test/ble_smoke_test.dart on the Windows
Flutter target. Pass the same QUICK_BLUE_SMOKE_* environment variables shown
in the example README to target a known BLE device. The first run installs
Windows, Visual Studio Build Tools, Git, and Flutter into persistent state under
.dart_tool/dockur_windows/. During that install it also registers a Windows
logon task so later runs reuse the same VM and execute the latest generated
test script from the shared folder. The guest keeps a synced NTFS checkout at
C:\quick_blue_workspace\quick_blue so Flutter can reuse .dart_tool and
build output between runs. Set QUICK_BLUE_WINDOWS_CLEAN_WORKTREE=1 to
recreate that checkout without reinstalling Windows.
Pigeon schemas and generated platform bindings must stay in sync. Regenerate after changing a Pigeon file:
cd quick_blue
dart run pigeon --input pigeons/messages.dart
cd ../quick_blue_darwin
dart run pigeon --input pigeons/messages.dartThis repository is licensed under the terms of LICENSE.