flutter_bluetooth_classic_serial 1.0.3 copy "flutter_bluetooth_classic_serial: ^1.0.3" to clipboard
flutter_bluetooth_classic_serial: ^1.0.3 copied to clipboard

A Flutter plugin for Bluetooth Classic communication on Android, iOS, and Windows platforms. Supports device discovery, connection management, and data transmission.

example/lib/main.dart

import 'dart:async';
import 'package:flutter/material.dart';
import 'package:flutter_bluetooth_classic_serial/flutter_bluetooth_classic.dart';

void main() {
  runApp(const MyApp());
}

class MyApp extends StatelessWidget {
  const MyApp({super.key});

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Bluetooth Classic Serial Demo',
      theme: ThemeData(
        colorScheme: ColorScheme.fromSeed(seedColor: Colors.blue),
        useMaterial3: true,
      ),
      home: const BluetoothClassicExample(),
    );
  }
}

class BluetoothClassicExample extends StatefulWidget {
  const BluetoothClassicExample({super.key});

  @override
  State<BluetoothClassicExample> createState() =>
      _BluetoothClassicExampleState();
}

class _BluetoothClassicExampleState extends State<BluetoothClassicExample>
    with WidgetsBindingObserver {
  late FlutterBluetoothClassic _bluetooth;
  bool _isBluetoothAvailable = false;
  bool _isBluetoothEnabled = false;
  BluetoothConnectionState? _connectionState;
  List<BluetoothDevice> _pairedDevices = [];
  List<BluetoothDevice> _discoveredDevices = [];
  BluetoothDevice? _connectedDevice;
  String _receivedData = '';
  final StringBuffer _dataBuffer = StringBuffer();
  final TextEditingController _messageController = TextEditingController();
  bool _isDiscovering = false;
  StreamSubscription<BluetoothConnectionState>? _connectionSubscription;
  StreamSubscription<BluetoothData>? _dataSubscription;
  StreamSubscription<BluetoothState>? _stateSubscription;
  Timer? _reconnectTimer;
  bool _autoReconnect = false;
  String? _lastConnectedAddress;
  @override
  void initState() {
    super.initState();
    WidgetsBinding.instance.addObserver(this);
    _bluetooth = FlutterBluetoothClassic();
    _initBluetooth();
  }

  @override
  void dispose() {
    WidgetsBinding.instance.removeObserver(this);
    _connectionSubscription?.cancel();
    _dataSubscription?.cancel();
    _stateSubscription?.cancel();
    _reconnectTimer?.cancel();
    _messageController.dispose();
    super.dispose();
  }

  @override
  void didChangeAppLifecycleState(AppLifecycleState state) {
    super.didChangeAppLifecycleState(state);
    if (state == AppLifecycleState.resumed) {
      _checkBluetoothState();
    }
  }

  Future<void> _initBluetooth() async {
    await _checkBluetoothState();
    _setupListeners();
  }

  Future<void> _checkBluetoothState() async {
    try {
      bool isSupported = await _bluetooth.isBluetoothSupported();
      bool isEnabled = await _bluetooth.isBluetoothEnabled();

      setState(() {
        _isBluetoothAvailable = isSupported;
        _isBluetoothEnabled = isEnabled;
      });

      if (isSupported && isEnabled) {
        await _loadPairedDevices();
      }
    } catch (e) {
      debugPrint('Error checking Bluetooth state: $e');
    }
  }

  void _setupListeners() {
    // Listen for Bluetooth state changes
    _stateSubscription = _bluetooth.onStateChanged.listen(
      (state) {
        setState(() {
          _isBluetoothEnabled = state.isEnabled;
        });
        if (state.isEnabled) {
          _loadPairedDevices();
        }
      },
      onError: (error) {
        debugPrint('Bluetooth state error: $error');
      },
    );

    // Listen for connection state changes
    _connectionSubscription = _bluetooth.onConnectionChanged.listen(
      _onConnectionStateChanged,
      onError: (error) {
        debugPrint('Connection state error: $error');
      },
    );

    // Listen for incoming data
    _dataSubscription = _bluetooth.onDataReceived.listen(
      _onDataReceived,
      onError: (error) {
        debugPrint('Data received error: $error');
      },
    );
  }

  void _onConnectionStateChanged(BluetoothConnectionState state) {
    setState(() {
      _connectionState = state;

      if (state.isConnected) {
        _connectedDevice = _pairedDevices.firstWhere(
          (device) => device.address == state.deviceAddress,
          orElse: () => _discoveredDevices.firstWhere(
            (device) => device.address == state.deviceAddress,
            orElse: () => BluetoothDevice(
              name: 'Unknown Device',
              address: state.deviceAddress,
              paired: false,
            ),
          ),
        );
        _lastConnectedAddress = state.deviceAddress;
        _reconnectTimer?.cancel();
      } else {
        _connectedDevice = null;
        if (_autoReconnect && _lastConnectedAddress != null) {
          _startReconnectTimer();
        }
      }
    });

    _showConnectionSnackBar(state);
  }

  void _onDataReceived(BluetoothData data) {
    String received = data.asString();
    _dataBuffer.write(received);

    setState(() {
      _receivedData += received;
    });

    // Process complete messages (assuming newline-terminated)
    String bufferContent = _dataBuffer.toString();
    if (bufferContent.contains('\n')) {
      List<String> lines = bufferContent.split('\n');
      _dataBuffer.clear();
      if (lines.isNotEmpty && lines.last.isNotEmpty) {
        _dataBuffer.write(lines.last);
      }

      // Process complete lines
      for (int i = 0; i < lines.length - 1; i++) {
        _processMessage(lines[i].trim());
      }
    }
  }

  void _processMessage(String message) {
    if (message.isNotEmpty) {
      debugPrint('Processed message: $message');
      // Add your message processing logic here
    }
  }

  void _showConnectionSnackBar(BluetoothConnectionState state) {
    if (!mounted) return;

    String message;
    Color backgroundColor;

    if (state.isConnected) {
      message = 'Connected to ${_connectedDevice?.name ?? 'device'}';
      backgroundColor = Colors.green;
    } else {
      message = 'Disconnected: ${state.status}';
      backgroundColor = Colors.red;
    }

    ScaffoldMessenger.of(context).showSnackBar(
      SnackBar(
        content: Text(message),
        backgroundColor: backgroundColor,
        duration: const Duration(seconds: 2),
      ),
    );
  }

  void _startReconnectTimer() {
    _reconnectTimer?.cancel();
    _reconnectTimer = Timer.periodic(const Duration(seconds: 5), (timer) async {
      if (_lastConnectedAddress != null &&
          _connectionState?.isConnected != true &&
          _autoReconnect) {
        try {
          debugPrint('Attempting reconnection to $_lastConnectedAddress');
          await _bluetooth.connect(_lastConnectedAddress!);
        } catch (e) {
          debugPrint('Reconnection failed: $e');
        }
      } else {
        timer.cancel();
      }
    });
  }

  Future<void> _loadPairedDevices() async {
    try {
      List<BluetoothDevice> devices = await _bluetooth.getPairedDevices();
      setState(() {
        _pairedDevices = devices;
      });
    } catch (e) {
      debugPrint('Error loading paired devices: $e');
      _showErrorSnackBar('Failed to load paired devices: $e');
    }
  }

  Future<void> _startDiscovery() async {
    if (!_isBluetoothEnabled) {
      _showErrorSnackBar('Bluetooth not enabled');
      return;
    }

    setState(() {
      _isDiscovering = true;
      _discoveredDevices.clear();
    });

    try {
      await _bluetooth.startDiscovery();

      // Stop discovery after 30 seconds
      Timer(const Duration(seconds: 30), () {
        _stopDiscovery();
      });
    } catch (e) {
      debugPrint('Error starting discovery: $e');
      setState(() {
        _isDiscovering = false;
      });
      _showErrorSnackBar('Failed to start discovery: $e');
    }
  }

  Future<void> _stopDiscovery() async {
    try {
      await _bluetooth.stopDiscovery();
      setState(() {
        _isDiscovering = false;
      });
    } catch (e) {
      debugPrint('Error stopping discovery: $e');
    }
  }

  Future<void> _connectToDevice(BluetoothDevice device) async {
    try {
      await _bluetooth.connect(device.address);
    } catch (e) {
      debugPrint('Error connecting to device: $e');
      _showErrorSnackBar('Failed to connect: $e');
    }
  }

  Future<void> _disconnect() async {
    try {
      _autoReconnect = false;
      _reconnectTimer?.cancel();
      await _bluetooth.disconnect();
    } catch (e) {
      debugPrint('Error disconnecting: $e');
      _showErrorSnackBar('Failed to disconnect: $e');
    }
  }

  Future<void> _sendMessage() async {
    if (_messageController.text.isEmpty) {
      _showErrorSnackBar('Please enter a message');
      return;
    }

    if (_connectionState?.isConnected != true) {
      _showErrorSnackBar('Not connected to a device');
      return;
    }

    try {
      String message = _messageController.text;
      await _bluetooth.sendString(message);
      setState(() {
        _receivedData += 'Sent: $message\n';
      });
      _messageController.clear();
    } catch (e) {
      debugPrint('Error sending message: $e');
      _showErrorSnackBar('Failed to send message: $e');
    }
  }

  void _showErrorSnackBar(String message) {
    if (!mounted) return;
    ScaffoldMessenger.of(context).showSnackBar(
      SnackBar(
        content: Text(message),
        backgroundColor: Colors.red,
        duration: const Duration(seconds: 3),
      ),
    );
  }

  void _clearReceivedData() {
    setState(() {
      _receivedData = '';
    });
    _dataBuffer.clear();
  }

  Future<void> _enableBluetooth() async {
    try {
      await _bluetooth.enableBluetooth();
      await _checkBluetoothState();
    } catch (e) {
      debugPrint('Error enabling Bluetooth: $e');
      _showErrorSnackBar('Failed to enable Bluetooth: $e');
    }
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: const Text('Bluetooth Classic Serial'),
        backgroundColor: Theme.of(context).colorScheme.inversePrimary,
        actions: [
          IconButton(
            onPressed: _checkBluetoothState,
            icon: const Icon(Icons.refresh),
            tooltip: 'Refresh',
          ),
        ],
      ),
      body: !_isBluetoothAvailable
          ? _buildBluetoothUnavailableView()
          : !_isBluetoothEnabled
          ? _buildBluetoothDisabledView()
          : _buildMainView(),
    );
  }

  Widget _buildBluetoothUnavailableView() {
    return Center(
      child: Padding(
        padding: const EdgeInsets.all(16.0),
        child: Column(
          mainAxisAlignment: MainAxisAlignment.center,
          children: [
            const Icon(Icons.bluetooth_disabled, size: 64, color: Colors.red),
            const SizedBox(height: 16),
            const Text(
              'Bluetooth Not Supported',
              style: TextStyle(fontSize: 24, fontWeight: FontWeight.bold),
            ),
            const SizedBox(height: 8),
            const Text(
              'This device does not support Bluetooth Classic.',
              textAlign: TextAlign.center,
            ),
          ],
        ),
      ),
    );
  }

  Widget _buildBluetoothDisabledView() {
    return Center(
      child: Padding(
        padding: const EdgeInsets.all(16.0),
        child: Column(
          mainAxisAlignment: MainAxisAlignment.center,
          children: [
            const Icon(Icons.bluetooth_disabled, size: 64, color: Colors.grey),
            const SizedBox(height: 16),
            const Text(
              'Bluetooth Disabled',
              style: TextStyle(fontSize: 24, fontWeight: FontWeight.bold),
            ),
            const SizedBox(height: 8),
            const Text(
              'Please enable Bluetooth to use this app.',
              textAlign: TextAlign.center,
            ),
            const SizedBox(height: 24),
            ElevatedButton(
              onPressed: _enableBluetooth,
              child: const Text('Enable Bluetooth'),
            ),
          ],
        ),
      ),
    );
  }

  Widget _buildMainView() {
    return Padding(
      padding: const EdgeInsets.all(16.0),
      child: Column(
        children: [
          _buildConnectionStatus(),
          const SizedBox(height: 16),
          _buildDeviceSection(),
          const SizedBox(height: 16),
          if (_connectionState?.isConnected == true) ...[
            _buildMessageSection(),
            const SizedBox(height: 16),
          ],
          Expanded(child: _buildDataSection()),
        ],
      ),
    );
  }

  Widget _buildConnectionStatus() {
    return Card(
      child: Padding(
        padding: const EdgeInsets.all(16.0),
        child: Column(
          crossAxisAlignment: CrossAxisAlignment.start,
          children: [
            Row(
              mainAxisAlignment: MainAxisAlignment.spaceBetween,
              children: [
                Text(
                  'Connection Status',
                  style: Theme.of(context).textTheme.titleMedium,
                ),
                Row(
                  children: [
                    const Text('Auto Reconnect'),
                    Switch(
                      value: _autoReconnect,
                      onChanged: (value) {
                        setState(() {
                          _autoReconnect = value;
                        });
                        if (!value) {
                          _reconnectTimer?.cancel();
                        }
                      },
                    ),
                  ],
                ),
              ],
            ),
            const SizedBox(height: 8),
            Row(
              children: [
                Icon(
                  _connectionState?.isConnected == true
                      ? Icons.link
                      : Icons.link_off,
                  color: _connectionState?.isConnected == true
                      ? Colors.green
                      : Colors.red,
                ),
                const SizedBox(width: 8),
                Text(_connectionState?.status ?? 'disconnected'),
              ],
            ),
            if (_connectedDevice != null) ...[
              const SizedBox(height: 8),
              Text('Connected to: ${_connectedDevice!.name}'),
              Text('Address: ${_connectedDevice!.address}'),
            ],
          ],
        ),
      ),
    );
  }

  Widget _buildDeviceSection() {
    return Card(
      child: Padding(
        padding: const EdgeInsets.all(16.0),
        child: Column(
          crossAxisAlignment: CrossAxisAlignment.start,
          children: [
            Row(
              mainAxisAlignment: MainAxisAlignment.spaceBetween,
              children: [
                Text('Devices', style: Theme.of(context).textTheme.titleMedium),
                Row(
                  children: [
                    IconButton(
                      onPressed: _isDiscovering ? null : _startDiscovery,
                      icon: _isDiscovering
                          ? const SizedBox(
                              width: 16,
                              height: 16,
                              child: CircularProgressIndicator(strokeWidth: 2),
                            )
                          : const Icon(Icons.search),
                      tooltip: 'Start discovery',
                    ),
                    IconButton(
                      onPressed: _loadPairedDevices,
                      icon: const Icon(Icons.refresh),
                      tooltip: 'Refresh paired devices',
                    ),
                  ],
                ),
              ],
            ),
            const SizedBox(height: 8),
            if (_pairedDevices.isNotEmpty) ...[
              const Text(
                'Paired Devices',
                style: TextStyle(fontWeight: FontWeight.bold),
              ),
              const SizedBox(height: 4),
              ..._pairedDevices.map((device) => _buildDeviceTile(device)),
            ],
            if (_discoveredDevices.isNotEmpty) ...[
              const SizedBox(height: 8),
              const Text(
                'Discovered Devices',
                style: TextStyle(fontWeight: FontWeight.bold),
              ),
              const SizedBox(height: 4),
              ..._discoveredDevices.map((device) => _buildDeviceTile(device)),
            ],
            if (_pairedDevices.isEmpty && _discoveredDevices.isEmpty)
              const Padding(
                padding: EdgeInsets.symmetric(vertical: 8.0),
                child: Text(
                  'No devices found. Try refreshing or starting discovery.',
                ),
              ),
          ],
        ),
      ),
    );
  }

  Widget _buildDeviceTile(BluetoothDevice device) {
    final isConnected = _connectedDevice?.address == device.address;
    final isConnecting =
        _connectionState?.deviceAddress == device.address &&
        !(_connectionState?.isConnected ?? false);

    return ListTile(
      leading: Icon(
        device.paired ? Icons.bluetooth : Icons.bluetooth_searching,
        color: isConnected
            ? Colors.green
            : (isConnecting ? Colors.orange : null),
      ),
      title: Text(device.name),
      subtitle: Text(device.address),
      trailing: isConnected
          ? ElevatedButton(
              onPressed: _disconnect,
              child: const Text('Disconnect'),
            )
          : ElevatedButton(
              onPressed: isConnecting ? null : () => _connectToDevice(device),
              child: Text(isConnecting ? 'Connecting...' : 'Connect'),
            ),
    );
  }

  Widget _buildMessageSection() {
    return Card(
      child: Padding(
        padding: const EdgeInsets.all(16.0),
        child: Column(
          crossAxisAlignment: CrossAxisAlignment.start,
          children: [
            Text(
              'Send Message',
              style: Theme.of(context).textTheme.titleMedium,
            ),
            const SizedBox(height: 8),
            Row(
              children: [
                Expanded(
                  child: TextField(
                    controller: _messageController,
                    decoration: const InputDecoration(
                      hintText: 'Enter message to send',
                      border: OutlineInputBorder(),
                    ),
                    onSubmitted: (_) => _sendMessage(),
                  ),
                ),
                const SizedBox(width: 8),
                ElevatedButton(
                  onPressed: _sendMessage,
                  child: const Text('Send'),
                ),
              ],
            ),
          ],
        ),
      ),
    );
  }

  Widget _buildDataSection() {
    return Card(
      child: Padding(
        padding: const EdgeInsets.all(16.0),
        child: Column(
          crossAxisAlignment: CrossAxisAlignment.start,
          children: [
            Row(
              mainAxisAlignment: MainAxisAlignment.spaceBetween,
              children: [
                Text(
                  'Received Data',
                  style: Theme.of(context).textTheme.titleMedium,
                ),
                IconButton(
                  onPressed: _clearReceivedData,
                  icon: const Icon(Icons.clear),
                  tooltip: 'Clear data',
                ),
              ],
            ),
            const SizedBox(height: 8),
            Expanded(
              child: Container(
                width: double.infinity,
                padding: const EdgeInsets.all(8.0),
                decoration: BoxDecoration(
                  border: Border.all(color: Colors.grey),
                  borderRadius: BorderRadius.circular(4.0),
                ),
                child: SingleChildScrollView(
                  child: Text(
                    _receivedData.isEmpty ? 'No data received' : _receivedData,
                    style: const TextStyle(fontFamily: 'monospace'),
                  ),
                ),
              ),
            ),
          ],
        ),
      ),
    );
  }
}
1
likes
145
points
122
downloads

Publisher

unverified uploader

Weekly Downloads

A Flutter plugin for Bluetooth Classic communication on Android, iOS, and Windows platforms. Supports device discovery, connection management, and data transmission.

Homepage
Repository (GitHub)
View/report issues

Topics

#bluetooth #bluetooth-classic #communication #serial #connectivity

Documentation

Documentation
API reference

License

MIT (license)

Dependencies

flutter, plugin_platform_interface

More

Packages that depend on flutter_bluetooth_classic_serial

Packages that implement flutter_bluetooth_classic_serial