bt_service
A production-ready Flutter plugin for Bluetooth Classic (RFCOMM) connections on Android. This plugin provides a simple and robust API for connecting to Bluetooth devices, sending and receiving data, with comprehensive error handling and thread-safe operations.
Features
- ✅ Connect to Bluetooth devices by MAC address
- ✅ Scan for nearby devices and discover available Bluetooth devices
- ✅ Get paired devices list
- ✅ Disconnect from connected devices
- ✅ Send data over Bluetooth connection
- ✅ Receive data via stream
- ✅ Connection state monitoring via stream
- ✅ Thread-safe operations - all Bluetooth operations run on background threads
- ✅ Robust error handling - comprehensive error messages and exception handling
- ✅ Production-ready - tested and optimized for reliability
Platform Support
- ✅ Android (API 24+)
- ❌ iOS (not supported - uses Classic Bluetooth, not BLE)
Installation
Add this to your package's pubspec.yaml file:
dependencies:
bt_service: ^0.0.1
Then run:
flutter pub get
Android Setup
Permissions
The plugin requires the following permissions in your AndroidManifest.xml:
<uses-permission android:name="android.permission.BLUETOOTH" />
<uses-permission android:name="android.permission.BLUETOOTH_ADMIN" />
<!-- Required for Android 12+ -->
<uses-permission android:name="android.permission.BLUETOOTH_CONNECT" />
<uses-permission android:name="android.permission.BLUETOOTH_SCAN" />
<!-- Required for Android < 12 scanning -->
<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" />
These permissions are automatically included in the plugin's manifest, but you may need to request runtime permissions in your app for Android 12+.
Runtime Permissions (Android 12+)
On Android 12 (API 31) and above, you need to request the BLUETOOTH_CONNECT and BLUETOOTH_SCAN permissions at runtime. For Android < 12, ACCESS_FINE_LOCATION is required for scanning.
Example using permission_handler:
import 'package:permission_handler/permission_handler.dart';
Future<bool> requestBluetoothPermission() async {
// Request multiple permissions at once
Map<Permission, PermissionStatus> statuses = await [
Permission.bluetooth,
Permission.bluetoothScan,
Permission.bluetoothConnect,
Permission.location,
].request();
return statuses.values.every((status) => status.isGranted);
}
Reality Check & Best Practices
This plugin is a lightweight wrapper around Android's Bluetooth Classic (RFCOMM) APIs. It provides raw access to sockets and streams.
When to use this plugin:
- You need to communicate with legacy Bluetooth devices (SPP) on Android.
- You are building an OBD-II, industrial, or custom hardware interface.
- You want full control over the byte stream.
Limitations:
- No built-in reconnection: If the connection drops, you must call
connect()again. Use the providedBtConnectionManagerfor auto-reconnection. - Raw Streams: Data is received as
Uint8Listchunks. You must handle framing (e.g., splitting by\r\n) yourself. - Android Only: iOS does not support SPP (Classic Bluetooth) for general developers (requires MFi).
Usage
Auto-Reconnection (Recommended)
For robust applications, use BtConnectionManager to handle connection drops automatically:
import 'package:bt_service/bt_service.dart';
final manager = BtConnectionManager(
address: '00:11:22:33:44:55',
retryInterval: Duration(seconds: 5),
maxRetries: 10,
);
// Start managing the connection
await manager.connect();
// Listen to state changes (includes 'reconnecting')
manager.state.listen((state) {
print('Connection state: $state');
});
// Listen to data
manager.data.listen((bytes) {
print('Received: ${String.fromCharCodes(bytes)}');
});
// Cleanup
await manager.disconnect();
Basic Example
import 'package:bt_service/bt_service.dart';
import 'dart:typed_data';
// Connect to a device
try {
await BtService.instance.connect('00:11:22:33:44:55');
print('Connected successfully!');
} catch (e) {
print('Connection failed: $e');
}
// Send data
try {
final message = 'Hello, Bluetooth!';
final bytes = Uint8List.fromList(message.codeUnits);
await BtService.instance.send(bytes);
print('Data sent successfully!');
} catch (e) {
print('Send failed: $e');
}
// Listen to incoming data
BtService.instance.onData.listen((data) {
print('Received ${data.length} bytes');
final text = String.fromCharCodes(data);
print('Content: $text');
});
// Listen to connection state changes
BtService.instance.onState.listen((state) {
print('Connection state: $state'); // 'connected', 'disconnected', or 'error'
});
// Disconnect
await BtService.instance.disconnect();
// Check connection status
final isConnected = await BtService.instance.isConnected();
// Start scanning for devices
await BtService.instance.startScan();
// Listen to discovered devices
BtService.instance.onDeviceDiscovered.listen((device) {
print('Found device: ${device['name']} (${device['address']})');
});
// Stop scanning
await BtService.instance.stopScan();
// Get paired devices
final pairedDevices = await BtService.instance.getPairedDevices();
Complete Example
import 'package:bt_service/bt_service.dart';
import 'dart:async';
import 'dart:typed_data';
class BluetoothManager {
StreamSubscription<Uint8List>? _dataSubscription;
StreamSubscription<String>? _stateSubscription;
void initialize() {
// Listen to incoming data
_dataSubscription = BtService.instance.onData.listen(
(data) {
print('Received: ${data.length} bytes');
// Process your data here
},
onError: (error) {
print('Data stream error: $error');
},
);
// Listen to connection state changes
_stateSubscription = BtService.instance.onState.listen(
(state) {
switch (state) {
case 'connected':
print('Device connected');
break;
case 'disconnected':
print('Device disconnected');
break;
case 'error':
print('Connection error occurred');
break;
}
},
);
}
Future<void> connectToDevice(String macAddress) async {
try {
await BtService.instance.connect(macAddress);
} catch (e) {
print('Failed to connect: $e');
rethrow;
}
}
Future<void> sendMessage(String message) async {
if (!await BtService.instance.isConnected()) {
throw Exception('Not connected to any device');
}
try {
final bytes = Uint8List.fromList(message.codeUnits);
await BtService.instance.send(bytes);
} catch (e) {
print('Failed to send: $e');
rethrow;
}
}
Future<void> disconnect() async {
await BtService.instance.disconnect();
}
void dispose() {
_dataSubscription?.cancel();
_stateSubscription?.cancel();
}
}
API Reference
BtService.instance
The singleton instance of the Bluetooth service.
Methods
Future<void> connect(String address)
Connects to a Bluetooth device by its MAC address.
Parameters:
address(String): The MAC address of the device (format:XX:XX:XX:XX:XX:XX)
Throws:
PlatformExceptionwith error codes:NO_ADAPTER: Device has no Bluetooth adapterBLUETOOTH_DISABLED: Bluetooth is not enabledPERMISSION_DENIED: Required permissions not grantedINVALID_ADDRESS: Invalid MAC address formatALREADY_CONNECTING: A connection attempt is already in progressALREADY_CONNECTED: Already connected to a deviceCONNECT_ERROR: Connection failed (check message for details)
Future<void> disconnect()
Disconnects from the currently connected device. Safe to call even if not connected.
Future<void> send(Uint8List bytes)
Sends data over the Bluetooth connection.
Parameters:
bytes(Uint8List): The data to send
Throws:
PlatformExceptionwith error codes:NOT_CONNECTED: No active connectionINVALID_ARGUMENT: Invalid or empty dataWRITE_ERROR: Failed to write data (connection may be lost)
Future<bool> isConnected()
Checks if currently connected to a device.
Returns: true if connected, false otherwise
Future<void> startScan()
Starts scanning for nearby Bluetooth devices. Discovered devices will be emitted on the onDeviceDiscovered stream.
Throws:
PlatformExceptionwith error codes:NO_ADAPTER: Device has no Bluetooth adapterBLUETOOTH_DISABLED: Bluetooth is not enabledPERMISSION_DENIED: Required permissions (BLUETOOTH_SCAN/LOCATION) not grantedALREADY_SCANNING: Scan is already in progress
Future<void> stopScan()
Stops an ongoing device scan.
Future<List<Map<String, dynamic>>> getPairedDevices()
Retrieves a list of devices already paired with the system.
Returns: List of maps containing name, address, and type.
Streams
Stream<Uint8List> onData
Stream of incoming data from the connected device. Emits Uint8List whenever data is received.
Stream<Map<String, dynamic>> onDeviceDiscovered
Stream of discovered devices during scanning. Emits a map containing:
name: Device name (or "Unknown Device")address: Device MAC addresstype: Device type integer
Stream<String> onState
Stream of connection state changes. Emits one of:
"connected": Successfully connected to a device"disconnected": Disconnected from the device"error": An error occurred
Error Handling
The plugin provides detailed error messages through PlatformException. Always wrap Bluetooth operations in try-catch blocks:
try {
await BtService.instance.connect(macAddress);
} on PlatformException catch (e) {
switch (e.code) {
case 'NO_ADAPTER':
// Handle no Bluetooth adapter
break;
case 'BLUETOOTH_DISABLED':
// Handle Bluetooth disabled
break;
case 'PERMISSION_DENIED':
// Handle permission denied
break;
case 'CONNECT_ERROR':
// Handle connection error
print('Error details: ${e.message}');
break;
default:
// Handle other errors
print('Unknown error: ${e.code} - ${e.message}');
}
}
Thread Safety
All Bluetooth operations (connect, send, receive) run on background threads to ensure smooth UI performance. The plugin uses thread-safe mechanisms internally to manage connections and data streams.
Limitations
- Android only: This plugin uses Classic Bluetooth (RFCOMM), which is not available on iOS
- No device discovery: This plugin now supports device discovery via
startScan! - RFCOMM only: Uses Serial Port Profile (SPP) UUID for connections
Testing
The plugin includes a comprehensive example app that demonstrates all features. To test:
-
Run the example app:
cd example flutter run -
Enter a valid Bluetooth device MAC address
-
Tap Connect
-
Send messages and observe incoming data
Contributing
Contributions are welcome! Please feel free to submit a Pull Request.
License
This project is licensed under the MIT License - see the LICENSE file for details.
Support
For issues, questions, or contributions, please visit the GitHub repository.