universal_bluetooth_classic 0.2.0 copy "universal_bluetooth_classic: ^0.2.0" to clipboard
universal_bluetooth_classic: ^0.2.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 (HID) ✔️ ✔️ ✔️
disconnect ✔️ ✔️² ✔️ ✔️ ✔️¹
sendReport ✔️ ✔️ ✔️
setupSdp / closeSdp ✔️ ✔️ ✔️
onDeviceDiscovered ✔️ ✔️ ✔️ ✔️
onDeviceRemoved ✔️ ✔️ ✔️
onConnectionStateChanged ✔️ ✔️ ✔️ ✔️
onGetReport ✔️ ✔️ ✔️
onSdpServiceRegistrationUpdate ✔️ ✔️ ✔️

¹ Linux supports a basic disconnect, not an HID-specific disconnect. ² On iOS, disconnect closes an External Accessory session.

Getting Started #

Add the package to your pubspec.yaml:

dependencies:
  universal_bluetooth_classic: ^0.2.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 Bluetooth HID 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. iOS uses the External Accessory framework instead. Linux supports only the basic disconnect operation.

HID Reports #

Send a report to a connected HID device:

import 'dart:typed_data';

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

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 the session using the first available protocol:

await UniversalBluetooth.disconnect();

The externalAccessory event payload is only available on iOS.

Data Types #

BluetoothDevice #

Discovered and paired devices expose:

  • address — the device address on most platforms, the connection ID on iOS
  • name
  • paired
  • isConnectedWithHid
  • rssi
  • deviceTypeclassic, 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.

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 #

BT Cam icon BT Cam
A Bluetooth remote for Canon, Nikon, Sony, Fujifilm, GoPro, Olympus, Panasonic, Pentax, and Blackmagic cameras.

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