bt_service 0.0.3 copy "bt_service: ^0.0.3" to clipboard
bt_service: ^0.0.3 copied to clipboard

PlatformAndroid

A production-ready Flutter plugin for Bluetooth Classic (RFCOMM) on Android. Supports connect, disconnect, and data transfer with robust error handling.

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 provided BtConnectionManager for auto-reconnection.
  • Raw Streams: Data is received as Uint8List chunks. 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 #

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:

  • PlatformException with error codes:
    • NO_ADAPTER: Device has no Bluetooth adapter
    • BLUETOOTH_DISABLED: Bluetooth is not enabled
    • PERMISSION_DENIED: Required permissions not granted
    • INVALID_ADDRESS: Invalid MAC address format
    • ALREADY_CONNECTING: A connection attempt is already in progress
    • ALREADY_CONNECTED: Already connected to a device
    • CONNECT_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:

  • PlatformException with error codes:
    • NOT_CONNECTED: No active connection
    • INVALID_ARGUMENT: Invalid or empty data
    • WRITE_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:

  • PlatformException with error codes:
    • NO_ADAPTER: Device has no Bluetooth adapter
    • BLUETOOTH_DISABLED: Bluetooth is not enabled
    • PERMISSION_DENIED: Required permissions (BLUETOOTH_SCAN/LOCATION) not granted
    • ALREADY_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 address
  • type: 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:

  1. Run the example app:

    cd example
    flutter run
    
  2. Enter a valid Bluetooth device MAC address

  3. Tap Connect

  4. 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.

0
likes
160
points
--
downloads

Publisher

unverified uploader

Weekly Downloads

A production-ready Flutter plugin for Bluetooth Classic (RFCOMM) on Android. Supports connect, disconnect, and data transfer with robust error handling.

Repository (GitHub)
View/report issues

Documentation

API reference

License

MIT (license)

Dependencies

flutter, plugin_platform_interface

More

Packages that depend on bt_service

Packages that implement bt_service