ble_plugin 1.5.0
ble_plugin: ^1.5.0 copied to clipboard
A simple and easy-to-use ble plugin.
example/lib/main.dart
import 'dart:io';
import 'dart:typed_data';
import 'package:ble_plugin/ble_device.dart';
import 'package:ble_plugin/ble_plugin.dart';
import 'package:ble_plugin/event_channel_constant.dart';
import 'package:ble_plugin_example/data_provider.dart';
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
void main() {
runApp(const MyApp());
}
class MyApp extends StatefulWidget {
const MyApp({super.key});
@override
State<MyApp> createState() => _MyAppState();
}
class _MyAppState extends State<MyApp> implements DeviceListener, DataListener {
final _blePlugin = BlePlugin.getInstance();
List<BleDevice> deviceList = [];
BleDevice? connectedDevice;
List<String> logs = [];
bool scanning = false;
int copies = 5;
String deviceId(BleDevice d) {
return d.mac?.isNotEmpty == true ? d.mac! : "${d.name}_${d.hashCode}";
}
void addLog(String log) {
debugPrint(log);
setState(() {
logs.insert(
0,
"[${DateTime.now().toString().substring(11, 19)}] $log",
);
if (logs.length > 200) {
logs.removeLast();
}
});
}
@override
void initState() {
super.initState();
_blePlugin.setDeviceListener(this);
_blePlugin.setDataListener(this);
}
@override
void dispose() {
_blePlugin.stopListener();
super.dispose();
}
@override
Widget build(BuildContext context) {
final bool isMac = Platform.isMacOS;
return MaterialApp(
debugShowCheckedModeBanner: false,
theme: ThemeData(
useMaterial3: true,
scaffoldBackgroundColor: const Color(0xfff5f7fb),
),
home: Scaffold(
appBar: AppBar(
elevation: 0,
centerTitle: true,
backgroundColor: Colors.white,
title: const Text(
"BLE Printer Demo",
style: TextStyle(
fontWeight: FontWeight.bold,
color: Colors.black87,
),
),
),
body: Center(
child: ConstrainedBox(
constraints: BoxConstraints(
maxWidth: isMac ? 1100 : double.infinity,
),
child: Column(
children: [
/// 顶部状态
Container(
margin: const EdgeInsets.fromLTRB(12, 10, 12, 8),
padding: const EdgeInsets.all(14),
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(18),
boxShadow: [
BoxShadow(
color: Colors.black.withOpacity(0.04),
blurRadius: 10,
)
],
),
child: Row(
children: [
Container(
padding: const EdgeInsets.all(12),
decoration: BoxDecoration(
color: connectedDevice != null
? Colors.green.withOpacity(0.12)
: Colors.blue.withOpacity(0.12),
borderRadius: BorderRadius.circular(14),
),
child: Icon(
Icons.bluetooth,
color: connectedDevice != null
? Colors.green
: Colors.blue,
),
),
const SizedBox(width: 14),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
connectedDevice?.name ?? "未连接设备",
maxLines: 1,
overflow: TextOverflow.ellipsis,
style: const TextStyle(
fontWeight: FontWeight.bold,
fontSize: 16,
),
),
const SizedBox(height: 4),
Text(
connectedDevice != null ? "设备已连接" : "等待连接打印机",
style: const TextStyle(
color: Colors.grey,
fontSize: 13,
),
),
],
),
),
if (connectedDevice != null)
FilledButton.tonal(
onPressed: () async {
await _blePlugin.disconnect();
},
child: const Text("断开"),
),
],
),
),
/// 操作栏
Padding(
padding: const EdgeInsets.symmetric(horizontal: 12),
child: Wrap(
spacing: 10,
runSpacing: 10,
children: [
_actionBtn(
icon: Icons.search,
text: scanning ? "扫描中" : "扫描",
color: Colors.blue,
onTap: () async {
setState(() {
scanning = true;
deviceList.clear();
});
addLog("开始扫描");
await _blePlugin.startScanBluetooth();
/// macOS 自动停止
if (Platform.isMacOS) {
Future.delayed(
const Duration(seconds: 8),
() async {
await _blePlugin.stopScanBluetooth();
if (mounted) {
setState(() {
scanning = false;
});
}
addLog("停止扫描");
},
);
}
},
),
_actionBtn(
icon: Icons.stop_circle_outlined,
text: "停止",
color: Colors.orange,
onTap: () async {
await _blePlugin.stopScanBluetooth();
setState(() {
scanning = false;
});
addLog("手动停止扫描");
},
),
_actionBtn(
icon: Icons.print,
text: "打印",
color: Colors.green,
onTap: printImage,
),
_actionBtn(
icon: Icons.send,
text: "原始数据",
color: Colors.purple,
onTap: () async {
await _blePlugin.sendUint8Command(
Uint8List.fromList([
16,
255,
48,
17,
]),
);
addLog("发送原始数据");
},
),
_actionBtn(
icon: Icons.info_outline,
text: "状态",
color: Colors.teal,
onTap: showStatusDialog,
),
],
),
),
const SizedBox(height: 10),
Padding(
padding: const EdgeInsets.symmetric(horizontal: 16),
child: Row(
children: [
const Icon(
Icons.devices,
size: 18,
color: Colors.black54,
),
const SizedBox(width: 6),
Text(
"蓝牙设备 (${deviceList.length})",
style: const TextStyle(
fontWeight: FontWeight.bold,
fontSize: 15,
),
),
],
),
),
const SizedBox(height: 8),
Expanded(
child: Row(
children: [
/// 设备列表
Expanded(
flex: 2,
child: deviceList.isEmpty
? _buildEmpty()
: ListView.builder(
padding: const EdgeInsets.only(
bottom: 20,
),
itemCount: deviceList.length,
itemBuilder: (_, index) {
return _buildDeviceItem(
deviceList[index],
);
},
),
),
/// mac 日志
if (isMac)
Container(
width: 360,
margin: const EdgeInsets.only(
right: 12,
bottom: 12,
),
decoration: BoxDecoration(
color: Colors.black,
borderRadius: BorderRadius.circular(16),
),
child: Column(
children: [
Container(
padding: const EdgeInsets.all(12),
child: const Row(
children: [
Icon(
Icons.terminal,
color: Colors.white,
size: 18,
),
SizedBox(width: 8),
Text(
"运行日志",
style: TextStyle(
color: Colors.white,
fontWeight: FontWeight.bold,
),
),
],
),
),
const Divider(
height: 1,
color: Colors.white24,
),
Expanded(
child: ListView.builder(
reverse: true,
padding: const EdgeInsets.all(10),
itemCount: logs.length,
itemBuilder: (_, index) {
return Padding(
padding: const EdgeInsets.only(
bottom: 4,
),
child: Text(
logs[index],
style: const TextStyle(
color: Colors.greenAccent,
fontSize: 12,
),
),
);
},
),
),
],
),
),
],
),
),
],
),
),
),
),
);
}
Widget _actionBtn({
required IconData icon,
required String text,
required Color color,
required VoidCallback onTap,
}) {
return InkWell(
borderRadius: BorderRadius.circular(12),
onTap: onTap,
child: Container(
height: 44,
padding: const EdgeInsets.symmetric(horizontal: 16),
decoration: BoxDecoration(
color: color.withOpacity(0.10),
borderRadius: BorderRadius.circular(12),
),
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
Icon(
icon,
size: 18,
color: color,
),
const SizedBox(width: 6),
Text(
text,
style: TextStyle(
color: color,
fontWeight: FontWeight.w600,
fontSize: 13,
),
),
],
),
),
);
}
Widget _buildDeviceItem(BleDevice device) {
bool isCurrent = connectedDevice != null &&
deviceId(connectedDevice!) == deviceId(device);
return Container(
margin: const EdgeInsets.symmetric(
horizontal: 12,
vertical: 5,
),
padding: const EdgeInsets.all(14),
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(16),
boxShadow: [
BoxShadow(
color: Colors.black.withOpacity(0.03),
blurRadius: 8,
offset: const Offset(0, 3),
),
],
),
child: Row(
children: [
Container(
padding: const EdgeInsets.all(12),
decoration: BoxDecoration(
color: isCurrent
? Colors.green.withOpacity(0.12)
: Colors.blue.withOpacity(0.08),
borderRadius: BorderRadius.circular(12),
),
child: Icon(
Icons.bluetooth,
color: isCurrent ? Colors.green : Colors.blue,
),
),
const SizedBox(width: 12),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
device.name.isEmpty ? "未知设备" : device.name,
maxLines: 1,
overflow: TextOverflow.ellipsis,
style: const TextStyle(
fontWeight: FontWeight.bold,
fontSize: 15,
),
),
const SizedBox(height: 4),
Text(
device.mac?.isNotEmpty == true
? device.mac!
: "macOS 无 MAC 地址",
style: const TextStyle(
color: Colors.grey,
fontSize: 12,
),
),
],
),
),
const SizedBox(width: 10),
FilledButton(
onPressed: () async {
addLog("开始连接: ${device.name}");
await _blePlugin.connect(device);
},
child: Text(
isCurrent ? "已连接" : "连接",
),
),
],
),
);
}
Widget _buildEmpty() {
return Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Icon(
Icons.bluetooth_disabled,
size: 72,
color: Colors.grey.shade400,
),
const SizedBox(height: 12),
Text(
scanning ? "正在搜索设备..." : "暂无蓝牙设备",
style: const TextStyle(
color: Colors.grey,
fontSize: 15,
),
),
],
),
);
}
Future<void> showStatusDialog() async {
bool isAvailable = await _blePlugin.isAvailable();
bool isConnected = await _blePlugin.isConnected();
bool isScanning = await _blePlugin.isScanning();
bool isEnable = await _blePlugin.isEnable();
BleDevice? device = await _blePlugin.getConnectedDevice();
if (!mounted) return;
showDialog(
context: context,
builder: (_) {
return AlertDialog(
title: const Text("设备状态"),
content: Text(
'''
蓝牙可用: $isAvailable
设备连接: $isConnected
正在扫描: $isScanning
蓝牙开启: $isEnable
连接设备: ${device?.name ?? '无'}
''',
),
);
},
);
}
Future<void> printImage() async {
if (copies < 1) return;
addLog("开始打印");
await _blePlugin.sendUint8TextCommand(
DataProvider.colorImage(),
);
setState(() {
copies--;
});
}
@override
void onFoundDevice(BleDevice device) {
bool exists = deviceList.any(
(e) => deviceId(e) == deviceId(device),
);
if (!exists) {
setState(() {
deviceList.add(device);
});
}
addLog("发现设备 : ${device.name}");
}
@override
void onReceivedData(Uint8List data) {
addLog("收到打印机数据 : ${data.length} bytes");
}
@override
void onConnectionStateChange(
int state,
BleDevice deviceBle,
) {
switch (state) {
case EventChannelConstant.STATE_CONNECTED:
addLog("连接成功 : ${deviceBle.name}");
break;
case EventChannelConstant.STATE_DISCONNECTED:
addLog("连接断开 : ${deviceBle.name}");
break;
case EventChannelConstant.STATE_FAILCONNECT:
addLog("连接失败 : ${deviceBle.name}");
break;
}
setState(() {
connectedDevice =
state == EventChannelConstant.STATE_CONNECTED ? deviceBle : null;
});
}
@override
void onBluetoothStateChange(int state) {
switch (state) {
case EventChannelConstant.BLUETOOTHON:
addLog("蓝牙开启");
break;
case EventChannelConstant.BLUETOOTHOFF:
addLog("蓝牙关闭");
break;
}
}
@override
void onWriteEnd(int state) {
addLog("数据写入完成");
}
}