Skip to main content

universal_bluetooth_classic 1.1.0 copy "universal_bluetooth_classic: ^1.1.0" to clipboard
universal_bluetooth_classic: ^1.1.0 copied to clipboard

Bluetooth Classic & External Accessory for Flutter

Universal Bluetooth Classic #

[Universal Bluetooth Classic — Bluetooth Classic and External Accessory for Flutter]

pub package Platform GitHub stars pub points Flutter Dart

A cross-platform Flutter plugin for discovering, pairing, and managing Bluetooth Classic accessories, Bluetooth HID devices, and Apple External Accessory sessions.

Looking for Bluetooth Low Energy? Check universal_ble.

Features #

API Support #

All APIs are called through the same UniversalBluetooth class. Platform differences are handled by the plugin; APIs that are not implemented on a platform throw UnimplementedError.

API Android iOS macOS Windows Linux
showBluetoothAccessoryPicker ✔️ ✔️ ✔️ ✔️ —
startScan / stopScan / isScanning ✔️ — ✔️ ✔️ ✔️
pair / unpair ✔️ — ✔️ ✔️ ✔️
getPairedDevices ✔️ — ✔️ ✔️ ✔️
connect ✔️ ✔️³ ✔️ — —
disconnect ✔️ ✔️¹ ✔️ — ✔️²
send ✔️ ✔️³ ✔️ — —
setupSdp / closeSdp ✔️ — ✔️ — —
onDeviceDiscovered ✔️ — ✔️ ✔️ ✔️
onDeviceRemoved ✔️ — ✔️ ✔️ —
onConnectionStateChanged ✔️ ✔️ ✔️ — ✔️
onData ✔️ ✔️³ ✔️ — —
onGetReport ✔️ — ✔️ — —
onSdpServiceRegistrationUpdate ✔️ — ✔️ — —

Legend: ✔️ supported, — not supported.

¹ On iOS, disconnect closes an External Accessory session. ² Linux supports a basic disconnect, not an HID-specific disconnect. ³ On iOS, connect opens an External Accessory session for a protocol string and send / onData exchange bytes over it. On the HID platforms onData delivers bytes received from the connected device.

Getting Started #

Add the package to your pubspec.yaml:

dependencies:
  universal_bluetooth_classic: ^1.1.0

Import it where you need it:

import 'package:universal_bluetooth_classic/universal_bluetooth_classic.dart';

Complete the setup for each target in Platform-specific setup before using the APIs below.

Scanning #

Register discovery callbacks before starting a scan:

UniversalBluetooth.onDeviceDiscovered = (device) {
  print('${device.name ?? 'Unknown'} (${device.address}), RSSI ${device.rssi}');
};

UniversalBluetooth.onDeviceRemoved = (device) {
  print('Removed: ${device.address}');
};

await UniversalBluetooth.startScan();

Check or stop the scan when needed:

final isScanning = await UniversalBluetooth.isScanning();

if (isScanning) {
  await UniversalBluetooth.stopScan();
}

Scanning is available on Android, macOS, Windows, and Linux.

Paired devices #

final devices = await UniversalBluetooth.getPairedDevices();

for (final device in devices) {
  print('${device.name ?? 'Unknown'} — ${device.address}');
}

Native accessory picker #

Open the platform's Bluetooth accessory picker. On iOS, this uses the External Accessory picker — the only way to use Bluetooth Classic MFi devices.

await UniversalBluetooth.showBluetoothAccessoryPicker();

Optionally filter by device name:

await UniversalBluetooth.showBluetoothAccessoryPicker(
  withNames: ['MyDevice', 'AnotherDevice'],
);

The native picker is not available on Linux.

Pairing #

Pair or unpair a device by its Bluetooth address:

final paired = await UniversalBluetooth.pair(
  '00:11:22:33:44:55',
);

if (paired) {
  print('Device paired');
}

await UniversalBluetooth.unpair('00:11:22:33:44:55');

Pairing APIs are available on Android, macOS, Windows, and Linux.

Connecting #

Connect to and disconnect from a device:

const deviceId = '00:11:22:33:44:55';

UniversalBluetooth.onConnectionStateChanged = (event) {
  print('${event.identifier}: ${event.state.name} (${event.source.name})');
};

await UniversalBluetooth.connect(deviceId);
await UniversalBluetooth.disconnect(deviceId);

HID connections are available on Android, macOS, and Windows. On iOS, connect takes an External Accessory protocol string and opens an EASession, which lets you exchange bytes with the accessory (see iOS External Accessory). Linux supports only the basic disconnect operation.

Sending and receiving data #

Send raw bytes to a connected device:

import 'dart:typed_data';

await UniversalBluetooth.send(
  '00:11:22:33:44:55',
  Uint8List.fromList([0x01, 0x02, 0x03]),
);

Receive bytes from a connected device:

UniversalBluetooth.onData = (identifier, data) {
  print('$identifier: $data');
};

On the HID platforms send writes an output report and onData delivers the bytes the connected device sends. On iOS both operate on the open External Accessory session. sendReport has been replaced by send.

Respond to HID get-report requests:

UniversalBluetooth.onGetReport =
    (deviceId, reportType, bufferSize) {
  return ReportReply(
    data: Uint8List.fromList([0x01, 0x02, 0x03]),
  );
};

HID reports are available on Android, macOS, and Windows.

SDP Service Registration #

Register a Bluetooth HID service with platform-specific configuration:

import 'dart:typed_data';

final config = SdpConfig(
  macSdpConfig: MacSdpConfig(
    data: {
      'ServiceName': 'My HID Service',
      // Add the remaining SDP properties.
    },
  ),
  androidSdpConfig: AndroidSdpConfig(
    name: 'My HID Service',
    description: 'HID Service Description',
    provider: 'My Company',
    subclass: 0x2540,
    descriptors: Uint8List.fromList([
      // Add the HID report descriptor.
    ]),
  ),
);

UniversalBluetooth.onSdpServiceRegistrationUpdate = (registered) {
  print('SDP service registered: $registered');
};

await UniversalBluetooth.setupSdp(config: config);

Close the registration when it is no longer needed:

await UniversalBluetooth.closeSdp();

SDP registration is available on Android, macOS, and Windows.

iOS External Accessory #

External Accessory sessions are iOS-only, but their connection changes use the same callback as the other platforms.

Pair Bluetooth Classic MFi devices through showBluetoothAccessoryPicker, which presents the system pairing UI for your declared UISupportedExternalAccessoryProtocols.

UniversalBluetooth.onConnectionStateChanged = (event) {
  final accessory = event.externalAccessory;
  if (accessory == null) return;

  print('${event.state.name}: ${accessory.name}');
  print('Manufacturer: ${accessory.manufacturer}');
  print('Protocols: ${accessory.protocolStrings}');
};

Close a session for a specific protocol:

await UniversalBluetooth.disconnect(
  'com.mycompany.myprotocol',
);

Omit the protocol string to close every open session:

await UniversalBluetooth.disconnect();

The externalAccessory event payload is only available on iOS.

Exchanging bytes #

Open a session for the accessory's protocol string, then send and receive bytes over it. For a serial accessory this is the raw byte channel:

import 'dart:typed_data';

const protocol = 'com.mycompany.myprotocol';

UniversalBluetooth.onData = (identifier, data) {
  print('$identifier received: $data');
};

// Opens an EASession for the protocol. The accessory must be connected and
// must advertise the protocol in its protocolStrings.
await UniversalBluetooth.connect(protocol);

// Send bytes. Frame them however the accessory expects.
await UniversalBluetooth.send(protocol, Uint8List.fromList('PING\r'.codeUnits));
await UniversalBluetooth.send(protocol, Uint8List.fromList([0x01, 0x02, 0x03]));

// Responses arrive through onData.

await UniversalBluetooth.disconnect(protocol);

The session uses the protocol string as its identifier, while onConnectionStateChanged reports the accessory-level connection (keyed by connection ID). A session that fails on its own surfaces as a disconnected event.

Data Types #

BluetoothDevice #

Discovered and paired devices expose:

  • address — the device address on most platforms, the connection ID on iOS
  • name
  • paired
  • isConnectedWithHid
  • rssi
  • deviceType — classic, le, dual, or unknown
  • deviceClass — for example peripheral, audioVideo, or computer

EAAccessory #

iOS External Accessory events provide the accessory name, manufacturer, model and serial numbers, firmware and hardware revisions, dock type, supported protocol strings, connection status, and connection ID.

BluetoothConnectionEvent #

Connection events provide an opaque identifier, a connected or disconnected state, and a source: hid, externalAccessory, or system. The External Accessory source also includes the full iOS EAAccessory value.

DataCallback #

onData delivers bytes as (String identifier, Uint8List data). The identifier is the External Accessory protocol string on iOS and the device ID elsewhere.

HID configuration #

  • SdpConfig holds the platform-specific MacSdpConfig and AndroidSdpConfig values used for service registration.
    • MacSdpConfig takes an optional sdpPlistFile path and a data map of SDP properties.
    • AndroidSdpConfig requires name, description, provider, subclass, and descriptors.
  • ReportReply returns optional report data or an optional HID error code from onGetReport.
  • ReportType identifies input, output, and feature reports.

Platform-specific setup #

Android #

Add the Bluetooth permissions to android/app/src/main/AndroidManifest.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.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" />

Set the minimum Android SDK to 23:

android {
    defaultConfig {
        minSdkVersion 23
    }
}

Request permissions at runtime. For Android 12 and newer, request Bluetooth scan and connect permissions. For Android 11 and older, request location permission. Packages such as permission_handler can handle these requests.

iOS #

Declare every External Accessory protocol supported by your accessory in ios/Runner/Info.plist:

<key>UISupportedExternalAccessoryProtocols</key>
<array>
    <string>com.yourcompany.yourapp.protocol</string>
</array>

macOS #

Add the Bluetooth capability to your macOS target in Xcode.

Windows #

The Bluetooth adapter must support Bluetooth 4.0 or newer. If the system has multiple adapters, the plugin uses the first adapter returned by Windows.

When packaging the app, declare the bluetooth and radios capabilities.

Linux #

The Bluetooth adapter must support Bluetooth 4.0 or newer. If the system has multiple adapters, the plugin uses the first adapter returned by BlueZ.

When distributing the app as a snap, add the bluez plug to snapcraft.yaml:

plugs:
  - bluez

Linux supports scanning, paired-device lookup, pairing, unpairing, basic disconnects, discovery callbacks, and system connection-state events. The native picker, HID connections and reports, and SDP registration are not implemented.

Customizing Platform Implementation #

Provide a custom implementation for testing or an unsupported platform by extending UniversalBluetoothInterface:

class UniversalBluetoothMock
    extends UniversalBluetoothInterface {
  // Override the APIs used by your application.
}

UniversalBluetooth.setInstance(
  UniversalBluetoothMock(),
);

Restore the default platform implementation with:

UniversalBluetooth.setInstance(null);

Example app #

The example project demonstrates discovery, pairing, HID connections, reports, SDP registration, and platform permission handling.

Run it on a connected device or desktop target:

cd example
flutter run

App showcase #

Built something with Universal Bluetooth? Open a pull request to add it here, including an SVG app icon.