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
- Platform support
- Roadmap
- Example
- Other useful links
- Installation
- Platform setup
- Getting started
- FAQ
- Support and feedback
- About
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.sppdefault โ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.
Other useful links
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:
Want to help? Pull requests are welcome โ see Support and feedback.
Libraries
- flutter_classic_bluetooth
- Flutter Classic Bluetooth โ Bluetooth Classic (RFCOMM) communication plugin.