my_bluetooth 2.0.0
my_bluetooth: ^2.0.0 copied to clipboard
Android Classic Bluetooth discovery, RFCOMM connections, and byte or file transfer for Flutter printer and accessory apps.
import 'dart:async';
import 'package:flutter/material.dart';
import 'package:image_picker/image_picker.dart';
import 'package:my_bluetooth/my_bluetooth.dart';
void main() {
runApp(const MyApp());
}
class MyApp extends StatelessWidget {
const MyApp({super.key});
@override
Widget build(BuildContext context) {
return const MaterialApp(home: BluetoothExamplePage());
}
}
class BluetoothExamplePage extends StatefulWidget {
const BluetoothExamplePage({super.key});
@override
State<BluetoothExamplePage> createState() => _BluetoothExamplePageState();
}
class _BluetoothExamplePageState extends State<BluetoothExamplePage> {
XFile? image;
final _myBluetooth = MyBluetooth();
MPBluetoothAdapterState _adapterState = MPBluetoothAdapterState.unknown;
StreamSubscription<MPBluetoothAdapterState>? _adapterStateStateSubscription;
bool _isScanning = false;
StreamSubscription<bool>? _isScanningSubscription;
MPConnectionStateEnum _connectState = MPConnectionStateEnum.disconnected;
StreamSubscription<ConnectionStateResponse>? _connectStateSubscription;
StreamSubscription<List<BluetoothDevice>>? _scanResultsSubscription;
List<BluetoothDevice> _scanResults = [];
BluetoothCapabilities? _capabilities;
BluetoothPermissionStatus? _scanPermissionStatus;
BluetoothScanSession? _scanSession;
StreamSubscription<List<BluetoothScanResult>>?
_scanSessionResultsSubscription;
final Map<String, int?> _rssiByRemoteId = {};
@override
void initState() {
super.initState();
if (!MyBluetooth.isSupported) return;
_adapterStateStateSubscription = _myBluetooth.adapterState.listen((state) {
_adapterState = state;
if (mounted) {
setState(() {});
}
}, onError: (e) {});
_connectStateSubscription = _myBluetooth.connectionState.listen((state) {
_connectState = state.connectionState;
if (mounted) {
setState(() {});
}
}, onError: (e) {});
_isScanningSubscription = _myBluetooth.discoveryState.listen((state) {
_isScanning = state;
if (mounted) {
setState(() {});
}
}, onError: (e) {});
_scanResultsSubscription = _myBluetooth.scanResults.listen((results) {
_scanResults = results;
if (mounted) {
setState(() {});
}
}, onError: (e) {});
}
@override
void dispose() {
_adapterStateStateSubscription?.cancel();
_scanResultsSubscription?.cancel();
_scanSessionResultsSubscription?.cancel();
_connectStateSubscription?.cancel();
_isScanningSubscription?.cancel();
final scanSession = _scanSession;
if (scanSession != null) {
unawaited(scanSession.stop().catchError((Object _) {}));
}
super.dispose();
}
Future<bool> _prepareScanPermission() async {
final capabilities = await _myBluetooth.getCapabilities();
if (mounted) {
setState(() => _capabilities = capabilities);
}
if (!capabilities.supported || !capabilities.adapterAvailable) {
if (mounted) {
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(content: Text('Bluetooth is unavailable.')),
);
}
return false;
}
var permissions = await _myBluetooth.checkPermissions();
var scanStatus = permissions.forOperation(BluetoothOperation.scan);
if (mounted) {
setState(() => _scanPermissionStatus = scanStatus);
}
if (scanStatus == BluetoothPermissionStatus.denied) {
if (!mounted) return false;
final accepted = await showDialog<bool>(
context: context,
builder: (context) => AlertDialog(
title: const Text('Nearby devices permission'),
content: const Text(
'Scanning is needed to find a Classic Bluetooth printer. '
'The example does not use scan results to derive location.',
),
actions: [
TextButton(
onPressed: () => Navigator.pop(context, false),
child: const Text('Cancel'),
),
FilledButton(
onPressed: () => Navigator.pop(context, true),
child: const Text('Continue'),
),
],
),
);
if (accepted != true) return false;
permissions = await _myBluetooth.requestPermissions(
BluetoothOperation.scan,
);
scanStatus = permissions.forOperation(BluetoothOperation.scan);
if (mounted) {
setState(() => _scanPermissionStatus = scanStatus);
}
}
return scanStatus == BluetoothPermissionStatus.granted ||
scanStatus == BluetoothPermissionStatus.notRequired;
}
Future<void> _startScanSession() async {
try {
if (!await _prepareScanPermission()) return;
await _scanSessionResultsSubscription?.cancel();
await _scanSession?.stop();
final session = await _myBluetooth.startScanSession(
options: const BluetoothScanOptions(
filter: BluetoothScanFilter(includeUnnamed: false),
timeout: Duration(seconds: 20),
staleAfter: Duration(seconds: 5),
maxResults: 20,
),
);
_scanSession = session;
_scanSessionResultsSubscription = session.results.listen((results) {
if (!mounted) return;
setState(() {
_scanResults = results.map((result) => result.device).toList();
_rssiByRemoteId
..clear()
..addEntries(
results.map(
(result) => MapEntry(result.device.remoteId, result.rssi),
),
);
});
});
} on MyBluetoothException catch (error) {
if (!mounted) return;
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text('Scan failed: ${error.kind.name}')),
);
}
}
Future<void> _stopDiscovery() async {
final session = _scanSession;
_scanSession = null;
await _scanSessionResultsSubscription?.cancel();
_scanSessionResultsSubscription = null;
if (session != null) {
await session.stop();
} else {
await _myBluetooth.stopScan();
}
}
@override
Widget build(BuildContext context) {
if (!MyBluetooth.isSupported) {
return const Scaffold(
body: Center(
child: Text('my_bluetooth supports Android Classic Bluetooth only.'),
),
);
}
return Scaffold(
appBar: AppBar(title: const Text('Plugin example app | My Bluetooth')),
body: Center(
child: SingleChildScrollView(
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
const Text("Step 1. On/Off Bluetooth"),
Text("State Bluetooth Adapter: ${_adapterState.toString()}"),
TextButton(
onPressed: () async {
await _myBluetooth.turnOn();
},
child: const Text("turnOn"),
),
const Text("Step 2: Scan device"),
Text(" is Scanning : ${_isScanning.toString()}"),
Text(
'Adapter available: '
'${_capabilities?.adapterAvailable ?? "not checked"}',
),
Text(
'Scan permission: '
'${_scanPermissionStatus?.name ?? "not checked"}',
),
const SizedBox(height: 20),
const Text("Step 3: Connect"),
Text("is connecting: $_connectState"),
TextButton(
onPressed: () async {
debugPrint('${await _myBluetooth.disconnect()}');
},
child: const Text("disconnect"),
),
const SizedBox(height: 20),
Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Expanded(
flex: 1,
child: Column(
mainAxisSize: MainAxisSize.max,
mainAxisAlignment: MainAxisAlignment.start,
children: [
TextButton(
onPressed: () async {
var bondedDevices =
await _myBluetooth.bondedDevices;
setState(() {
_scanResults = bondedDevices;
});
debugPrint(
'bondedDevices: ${bondedDevices.length}',
);
},
child: const Text("getBondedDevices"),
),
TextButton(
onPressed: () async {
await _scanSessionResultsSubscription?.cancel();
_scanSessionResultsSubscription = null;
_scanSession = null;
_rssiByRemoteId.clear();
await _myBluetooth.startScan(
withKeywords: [
// "Nailpop", "Nailpop Pro",
// "Snap# Kiosk", "DMP"
// "image box"
],
);
},
child: const Text("startScan"),
),
TextButton(
onPressed: _startScanSession,
child: const Text("startScanSession (V2)"),
),
TextButton(
onPressed: () async {
await _stopDiscovery();
},
child: const Text("stopScan"),
),
],
),
),
SizedBox(
width: MediaQuery.of(context).size.width / 3 * 2,
height: 140,
child: ListView.builder(
scrollDirection: Axis.horizontal,
shrinkWrap: true,
itemCount: _scanResults.length,
itemBuilder: (BuildContext context, int index) {
final item = _scanResults[index];
return GestureDetector(
onTap: () async {
debugPrint('click ${item.remoteId}');
debugPrint(
'${await _myBluetooth.connect(remoteId: item.remoteId)}',
);
},
child: Container(
margin: const EdgeInsets.symmetric(horizontal: 20),
padding: const EdgeInsets.all(20),
decoration: BoxDecoration(
color: Colors.blueAccent.withValues(alpha: 0.2),
),
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
Text(
"Name: ${item.platformName}",
style: const TextStyle(
fontWeight: FontWeight.bold,
),
),
Text("ID: ${item.remoteId}"),
Text("bondState: ${item.bondState}"),
Text("type: ${item.type}"),
Text(
"RSSI: "
"${_rssiByRemoteId[item.remoteId] ?? 'n/a'}",
),
],
),
),
);
},
),
),
],
),
const Text(
"SEND TEXT TO DEVICE",
style: TextStyle(fontWeight: FontWeight.w800),
),
TextButton(
onPressed: () async {
await _myBluetooth.sendText(value: "Hello World");
},
child: const Text("1 | Send text 'Hello World'"),
),
//Send File
const Text(
"HOW TO SEND FILE?",
style: TextStyle(fontWeight: FontWeight.w800),
),
TextButton(
onPressed: () async {
final ImagePicker picker = ImagePicker();
image = await picker.pickImage(source: ImageSource.gallery);
debugPrint(image?.path);
},
child: const Text("Step 1 | Choose file"),
),
const SizedBox(height: 10),
TextButton(
onPressed: () async {
if (image != null) {
debugPrint(
'${await _myBluetooth.sendFile(pathImage: image?.path)}',
);
} else {
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(
content: Text("You need to choose file first"),
),
);
}
},
child: const Text("Step 3 | Sent file"),
),
//Send File
const Text(
"NOTE!!!! You can use function 'sendCmd' to send any thing.",
style: TextStyle(fontWeight: FontWeight.w500),
),
],
),
),
),
);
}
}