pub version pub points pub likes GitHub stars GitHub forks GitHub issues CI status Last commit License: MIT Dart Flutter

Bluetooth Classic Serial (RFCOMM/SPP) Plugin for Flutter

flutter_classic_bluetooth is a Flutter plugin for Bluetooth Classic serial communication over RFCOMM (the Serial Port Profile, SPP). It lets you discover, pair, connect to, and exchange data with Bluetooth Classic devices โ€” ESP32, ESP8266, Arduino boards, HC-05 / HC-06 modules, barcode scanners, thermal printers, OBD-II adapters, and other serial / UART peripherals โ€” from a single Dart API on Android, Windows, macOS, Linux, and iOS (MFi). Connections are exposed as Dart streams, so reading and writing bytes feels like any other Stream/Sink.

โญ Find this useful? Star it on GitHub and ๐Ÿ‘ like it on pub.dev โ€” it helps other Flutter developers find a maintained Bluetooth Classic plugin.

Overview

flutter_classic_bluetooth speaks RFCOMM/SPP, the classic Bluetooth serial transport โ€” not Bluetooth Low Energy (BLE). It wraps each platform's native stack (Android BluetoothSocket, Windows Winsock2 AF_BTH, Linux BlueZ RFCOMM sockets, macOS IOBluetooth, iOS ExternalAccessory) behind one consistent Dart interface. You can act as a client (connect out to a device) or as a server (advertise an SDP service and accept incoming connections), run several connections at once, and observe adapter, discovery, and bond state through broadcast streams.

What you can do with it:

  • Discover nearby devices and list paired/bonded devices.
  • Connect over RFCOMM by MAC address + service UUID, then read and write a byte stream.
  • Run an RFCOMM server that accepts incoming client connections.
  • Pair/unpair devices, toggle the adapter, and make the device discoverable (where the platform allows).
  • Query per-platform capabilities at runtime so your UI only offers what works.

Table of contents

Key features

A complete Bluetooth Classic (RFCOMM/SPP) client + server toolkit behind one Dart API. Expand a group for details:

๐Ÿ“ก Connectivity
  • RFCOMM/SPP client โ€” connect by address + service UUID, secure or insecure
  • RFCOMM server โ€” advertise an SDP service and accept incoming clients
  • Multiple simultaneous connections, each with its own id
  • Optional connection timeout
  • Optional auto-reconnect with exponential backoff (connectWithReconnect)
๐Ÿ” Discovery & pairing
  • Device discovery with results and start/stop state streams
  • Paired/bonded device listing
  • Bond / unbond devices and observe bond-state changes
  • Make discoverable (where supported)
๐Ÿ”€ Streamed I/O
  • Incoming bytes as a Dart Stream<Uint8List>
  • Ordered write sink โ€” add, writeBytes, writeString, addStream, allSent
  • Connection-state stream (connecting โ†’ connected โ†’ disconnecting โ†’ disconnected)
๐Ÿงฉ Adapter & capabilities
  • Adapter state stream, name, and address
  • Enable/disable the adapter (Android)
  • Runtime platform-capability matrix so the UI adapts per platform
๐Ÿ›ก๏ธ Reliability
  • Typed exception hierarchy โ€” BtcException + subtypes
  • Main-thread-safe event delivery on every platform
  • Honest per-platform capability reporting (no dead code paths)

Platform support

Bluetooth Classic capabilities differ by OS, so the plugin reports what each one can actually do (also queryable at runtime via getPlatformCapabilities()):

Feature Android Windows macOS Linux iOS
Adapter state stream โœ… โœ… โœ… โœ… โœ…
Discover devices โœ… โœ… โœ… โœ… โŒ
Get paired devices โœ… โœ… โœ… โœ… โœ…ยน
Pair (bond) โœ… โœ… โœ…ยฒ โœ…ยณ โŒ
Unpair (unbond) โœ… โœ… โŒโด โœ…ยณ โŒ
Connect (RFCOMM) โœ… โœ… โœ… โœ… โœ…ยน
Server mode โœ… โœ… โœ… โœ… โŒ
Enable / Disable โœ… โŒ โŒ โœ…ยณ โŒ
Set discoverable โœ… โœ… โŒ โœ… โŒ

ยน iOS uses the ExternalAccessory framework โ€” only MFi-certified accessories are supported, and the uuid argument is treated as the MFi protocol string.
ยฒ macOS pairs via IOBluetoothDevicePair and may show a system pairing prompt.
ยณ Linux uses the BlueZ D-Bus API (org.bluez); pairing a device that needs a PIN/passkey requires a system pairing agent.
โด macOS has no public API to remove an existing pairing โ€” unpair via System Settings.

Roadmap

What's shipped and what's next. Completed items are checked; the rest is on the list โ€” contributions welcome.

Shipped

  • โœ… RFCOMM/SPP client โ€” connect by address + UUID, secure/insecure, optional timeout
  • โœ… RFCOMM server โ€” advertise an SDP service and accept incoming clients
  • โœ… Multiple simultaneous connections, each with its own id
  • โœ… Device discovery with results and start/stop state streams
  • โœ… Paired/bonded device listing
  • โœ… Pair / unpair with a bond-state stream
  • โœ… Adapter state stream, enable/disable, and set discoverable
  • โœ… Streamed byte I/O with an ordered write sink (writeString / writeBytes / addStream)
  • โœ… Connection-state lifecycle stream
  • โœ… Runtime platform-capability matrix
  • โœ… Typed exception hierarchy (BtcException + subtypes)
  • โœ… BtcUuid.spp default โ€” connect(address: ...) just works for serial devices
  • โœ… Optional auto-reconnect with exponential backoff (connectWithReconnect)
  • โœ… Five platforms โ€” Android, Windows, macOS, Linux, iOS (MFi)
  • โœ… Linux via BlueZ D-Bus โ€” discovery, adapter and pairing work without root

Planned

  • โฌœ Live RSSI updates on an active connection
  • โฌœ Linux: built-in pairing agent for PIN/passkey devices
  • โฌœ macOS: programmatic unpair (pending a public Apple API)
  • โฌœ Expanded on-device integration tests

Out of scope โ€” use a dedicated package instead: Bluetooth Low Energy (BLE), and Web (Bluetooth Classic is not available in browsers).

Example

A complete, runnable demo app lives in the example/ directory, with screens for adapter control, device discovery, paired devices, RFCOMM client/server, and the platform-capabilities matrix. Clone the repository and run it, or copy any snippet from Getting started below.

Installation

flutter pub add flutter_classic_bluetooth

Then import it:

import 'package:flutter_classic_bluetooth/flutter_classic_bluetooth.dart';

Platform setup

Android โ€” add the Bluetooth permissions to android/app/src/main/AndroidManifest.xml:

<!-- Android 11 (API 30) and below -->
<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" />
<!-- Android 12 (API 31) and above -->
<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_ADVERTISE" />

iOS โ€” declare the MFi protocol(s) and a usage string in ios/Runner/Info.plist:

<key>UISupportedExternalAccessoryProtocols</key>
<array>
  <string>com.example.spp</string>
</array>
<key>NSBluetoothAlwaysUsageDescription</key>
<string>This app communicates with Bluetooth accessories.</string>

macOS โ€” add the Bluetooth entitlement to macos/Runner/*.entitlements and a usage string to Info.plist:

<key>com.apple.security.device.bluetooth</key>
<true/>

Linux โ€” install the GTK and BlueZ development packages the native plugin builds against (Debian/Ubuntu shown; the build fails with a gtk+-3.0 or bluetooth/bluetooth.h CMake error if they're missing):

sudo apt-get install -y libgtk-3-dev libbluetooth-dev ninja-build cmake pkg-config clang

On Fedora use gtk3-devel bluez-libs-devel ninja-build cmake clang; on Arch, gtk3 bluez-libs ninja cmake clang.

Getting started

Initialize and check support

final bluetooth = FlutterClassicBluetooth();

final supported = await bluetooth.isSupported();
final enabled = await bluetooth.isEnabled();
final caps = await bluetooth.getPlatformCapabilities();

if (caps.canDiscoverDevices) {
  // safe to call startDiscovery() on this platform
}

Discover nearby devices

final sub = bluetooth.discoveryResults.listen((device) {
  print('Found: ${device.displayName} (${device.address})');
});

await bluetooth.startDiscovery();
// ...later
await bluetooth.stopDiscovery();
await sub.cancel();

List paired devices

final devices = await bluetooth.getPairedDevices();
for (final device in devices) {
  print('${device.displayName} โ€” ${device.address} [${device.bondState.name}]');
}

Connect to a device

// SPP is the default โ€” for HC-05/06, ESP32, Arduino, etc. this is all you need:
final connection = await bluetooth.connect(address: 'AA:BB:CC:DD:EE:FF');
print('Connected: id=${connection.id}');

// Override the UUID and tune the attempt only when you need to:
final custom = await bluetooth.connect(
  address: 'AA:BB:CC:DD:EE:FF',
  uuid: BtcUuid.spp, // or any service UUID string
  secure: true,
  timeout: const Duration(seconds: 15), // optional
);

Receive data

connection.input.listen(
  (Uint8List data) => print('Received ${data.length} bytes: $data'),
  onDone: () => print('Remote closed the connection'),
);

Send data

// Raw bytes
await connection.output.add(Uint8List.fromList([0x01, 0x02, 0x03]));

// Convenience helpers
await connection.output.writeBytes([0x04, 0x05]);
await connection.output.writeString('AT+RESET\r\n');

// Wait until everything queued so far has been written
await connection.output.allSent;

Watch the connection state

connection.stateStream.listen((state) {
  print('State: ${state.name}'); // connected, disconnecting, disconnected, ...
});

print(connection.isConnected); // true while connected

Disconnect and dispose

await connection.finish(); // flush pending writes, then disconnect
// or: await connection.close(); // disconnect immediately

connection.dispose(); // always release resources when done

Reconnect automatically

For long-lived links to a flaky device, connectWithReconnect keeps the connection alive across drops. Its input and state streams are stable โ€” subscribe once and keep receiving data as the underlying connection is replaced.

final link = bluetooth.connectWithReconnect(
  address: 'AA:BB:CC:DD:EE:FF',
  policy: const BtcReconnectPolicy(
    maxAttempts: null, // retry forever (default)
    initialBackoff: Duration(seconds: 1),
    maxBackoff: Duration(seconds: 30),
  ),
);

link.state.listen((s) => print('Link: ${s.name}')); // connecting/connected/reconnecting/โ€ฆ
link.input.listen((bytes) => print('RX ${bytes.length} bytes'));

if (link.isConnected) await link.sendString('PING\r\n');

// ...when done
await link.close(); // stop reconnecting and release everything

Run an RFCOMM server

final server = await bluetooth.startServer(
  serviceName: 'MyService',
  uuid: BtcUuid.spp, // optional โ€” SPP is the default
  secure: true,
);

server.connections.listen((client) {
  print('Client connected: ${client.address}');
  client.input.listen((data) => client.output.writeString('echo: '));
});

// ...later
await server.close();

Pair and unpair

if (caps.canBondDevices) {
  final ok = await bluetooth.bondDevice('AA:BB:CC:DD:EE:FF');
  print('Bonded: $ok');
}

bluetooth.bondState('AA:BB:CC:DD:EE:FF').listen((state) {
  print('Bond state: ${state.name}');
});

await bluetooth.unbondDevice('AA:BB:CC:DD:EE:FF');

Adapter state and control

bluetooth.adapterState.listen((state) {
  print('Adapter: ${state.name}'); // on, off, turningOn, ...
});

if (caps.canEnableBluetooth) {
  await bluetooth.enableBluetooth(); // Android: shows the system dialog
}

if (caps.canSetDiscoverable) {
  await bluetooth.setDiscoverable(120); // seconds
}

Handle errors

try {
  await bluetooth.connect(address: addr, uuid: uuid);
} on BtcUnsupportedException catch (e) {
  print('${e.feature} not supported on ${e.platform}');
} on BtcDisabledException {
  print('Turn on Bluetooth first');
} on BtcTimeoutException {
  print('Connection timed out');
} on BtcConnectionException catch (e) {
  print('Connection failed: ${e.message}');
} on BtcException catch (e) {
  print('Bluetooth error: ${e.message}');
}

Every failure throws a typed BtcException (or a subtype): BtcUnsupportedException, BtcPermissionException, BtcDisabledException, BtcConnectionException, BtcWriteException, BtcDiscoveryException, BtcTimeoutException, BtcAddressException, and BtcUuidException.

FAQ

Is this Bluetooth Classic or Bluetooth Low Energy (BLE)? Bluetooth Classic โ€” RFCOMM/SPP serial communication. For BLE, use a BLE-specific package; this plugin targets classic serial peripherals like ESP32, HC-05/HC-06, printers, and scanners.

Does it work with ESP32, ESP8266, Arduino, and HC-05/HC-06 modules? Yes. Any device that exposes a Bluetooth Classic RFCOMM/SPP serial profile works: an ESP32 using BluetoothSerial, an Arduino or ESP8266 wired to an HC-05/HC-06 module, and other UART-over-Bluetooth peripherals (thermal printers, barcode scanners, OBD-II adapters). Pair the device, then just connect(address: ...) โ€” the SPP UUID (BtcUuid.spp) is used by default.

Which platforms are supported? Android, Windows, macOS, and Linux for full client/server RFCOMM; iOS supports only MFi-certified accessories via the ExternalAccessory framework (no discovery or server mode). See Platform support.

Why does iOS behave differently? Apple restricts general Bluetooth Classic access to MFi-certified accessories. On iOS the uuid you pass to connect() is treated as the MFi protocol string, and discovery/pairing/server features are unavailable by platform design.

Can I have several connections open at once? Yes. Each connect() (and each accepted server client) returns an independent BtcConnection with its own input/output streams.

How do I know if a feature works on the current device? Call getPlatformCapabilities() and check the matching flag (e.g. canDiscoverDevices, canCreateServer) before invoking it โ€” the plugin reports capabilities honestly per platform.

How does pairing work on macOS and Linux? On macOS, bondDevice pairs via IOBluetoothDevicePair (which may show a system prompt for PIN/passkey devices); removing a pairing has no public API, so unpair through System Settings. On Linux, bondDevice/unbondDevice use the BlueZ D-Bus API directly; devices that require a PIN or passkey additionally need a system pairing agent (e.g. a running desktop Bluetooth applet).

Support and feedback

  • Found a bug or want a feature? Open an issue on the issue tracker.
  • Questions and ideas are welcome via GitHub Discussions.
  • Pull requests are welcome โ€” see the repository for contribution guidelines.

About

flutter_classic_bluetooth is an open-source, MIT-licensed Flutter plugin for Bluetooth Classic (RFCOMM/SPP) serial communication across Android, Windows, macOS, Linux, and iOS (MFi), exposing native Bluetooth stacks through one stream-based Dart API.

flutter_classic_bluetooth is created and owned by Nurullah Al Masum.

Contributors

flutter_classic_bluetooth grows with its community โ€” every contributor is listed here:

flutter_classic_bluetooth contributors

Want to help? Pull requests are welcome โ€” see Support and feedback.

Libraries

flutter_classic_bluetooth
Flutter Classic Bluetooth โ€” Bluetooth Classic (RFCOMM) communication plugin.