posprinter_usb_android 1.0.0
posprinter_usb_android: ^1.0.0 copied to clipboard
Android USB host transport for ESC/POS thermal receipt printers: device discovery, the USB permission handshake and raw bulk-transfer printing.
import 'dart:async';
import 'package:flutter/material.dart';
import 'package:posprinter_usb_android/posprinter_usb_android.dart';
void main() => runApp(const ExampleApp());
class ExampleApp extends StatelessWidget {
const ExampleApp({super.key});
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'USB printer example',
debugShowCheckedModeBanner: false,
theme: ThemeData(
useMaterial3: true,
colorScheme: ColorScheme.fromSeed(seedColor: const Color(0xFF2563EB)),
),
home: const UsbPrinterPage(),
);
}
}
class UsbPrinterPage extends StatefulWidget {
const UsbPrinterPage({super.key});
@override
State<UsbPrinterPage> createState() => _UsbPrinterPageState();
}
class _UsbPrinterPageState extends State<UsbPrinterPage> {
final PrinterManager _manager = PrinterManager.instance;
final List<PrinterDevice> _printers = <PrinterDevice>[];
StreamSubscription<PrinterDevice>? _discoverySubscription;
StreamSubscription<USBStatus>? _statusSubscription;
PrinterDevice? _connectedPrinter;
USBStatus _status = USBStatus.none;
bool _busy = false;
@override
void initState() {
super.initState();
_statusSubscription = _manager.stateUSB.listen((USBStatus status) {
if (!mounted) return;
setState(() {
_status = status;
if (status == USBStatus.none) _connectedPrinter = null;
});
});
// Deferred to the first frame: the plugin's discovery stream never
// completes, and `_scan` toggles state as soon as it starts.
WidgetsBinding.instance.addPostFrameCallback((Duration _) {
if (mounted) unawaited(_scan());
});
}
@override
void dispose() {
_discoverySubscription?.cancel();
_statusSubscription?.cancel();
super.dispose();
}
Future<void> _scan() async {
if (_busy) return;
setState(() {
_busy = true;
_printers.clear();
});
await _discoverySubscription?.cancel();
_discoverySubscription = _manager
.discovery(type: PrinterType.usb)
.listen((PrinterDevice device) {
if (!mounted) return;
setState(() => _printers.add(device));
});
// The platform discovery stream has no completion event: give it a moment,
// then re-enable the UI.
await Future<void>.delayed(const Duration(seconds: 3));
if (mounted) setState(() => _busy = false);
}
Future<void> _connect(PrinterDevice device) async {
if (_busy) return;
setState(() => _busy = true);
final bool connected = await _manager.connect(
type: PrinterType.usb,
model: UsbPrinterInput(
name: device.name,
vendorId: device.vendorId,
productId: device.productId,
),
);
if (!mounted) return;
setState(() {
_busy = false;
if (connected) _connectedPrinter = device;
});
_showMessage(
connected
? 'Connected to ${device.name}.'
: 'Could not connect to ${device.name}. Replug the cable and retry.',
);
}
Future<void> _printTest() async {
if (_busy) return;
setState(() => _busy = true);
final bool sent = await _manager.send(
type: PrinterType.usb,
bytes: _testReceipt(),
);
if (!mounted) return;
setState(() => _busy = false);
_showMessage(sent ? 'Test receipt sent.' : 'Print failed.');
}
Future<void> _disconnect() async {
if (_busy) return;
setState(() => _busy = true);
await _manager.disconnect(type: PrinterType.usb);
if (!mounted) return;
setState(() {
_busy = false;
_connectedPrinter = null;
});
}
void _showMessage(String message) {
if (!mounted) return;
ScaffoldMessenger.of(context)
..hideCurrentSnackBar()
..showSnackBar(SnackBar(content: Text(message)));
}
/// Minimal ESC/POS test page: reset, centre, text, feed, cut.
List<int> _testReceipt() {
final List<int> bytes = <int>[
0x1B, 0x40, // reset
0x1B, 0x61, 0x01, // centre
];
bytes.addAll('USB PRINT TEST\n'.codeUnits);
bytes.addAll(<int>[0x1B, 0x61, 0x00]); // left
bytes.addAll('Printer: ${_connectedPrinter?.name ?? 'unknown'}\n'.codeUnits);
bytes.addAll('Printed: ${DateTime.now()}\n'.codeUnits);
bytes.addAll(<int>[0x0A, 0x0A, 0x0A]);
bytes.addAll(<int>[0x1D, 0x56, 0x00]); // cut
return bytes;
}
bool _isConnected(PrinterDevice device) {
final PrinterDevice? current = _connectedPrinter;
if (current == null) return false;
if (current.vendorId != null && device.vendorId != null) {
return current.vendorId == device.vendorId &&
current.productId == device.productId;
}
return current.name == device.name;
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: const Text('USB thermal printer')),
body: SafeArea(
child: Column(
children: <Widget>[
Padding(
padding: const EdgeInsets.all(12),
child: Row(
children: <Widget>[
Expanded(
child: FilledButton.icon(
onPressed: _busy ? null : _scan,
icon: const Icon(Icons.usb_rounded),
label: Text(_busy ? 'Working…' : 'Scan for printers'),
),
),
],
),
),
Padding(
padding: const EdgeInsets.symmetric(horizontal: 12),
child: Align(
alignment: Alignment.centerLeft,
child: Text(
'USB state: ${_status.name}',
style: Theme.of(context).textTheme.bodySmall,
),
),
),
Expanded(
child: _printers.isEmpty
? const Center(
child: Padding(
padding: EdgeInsets.all(24),
child: Text(
'Attach a USB receipt printer, then tap Scan. '
'Android will ask for permission the first time you '
'connect.',
textAlign: TextAlign.center,
),
),
)
: ListView.builder(
itemCount: _printers.length,
itemBuilder: (BuildContext context, int index) {
final PrinterDevice device = _printers[index];
final bool connected = _isConnected(device);
return ListTile(
leading: Icon(
connected
? Icons.usb_rounded
: Icons.print_rounded,
),
title: Text(device.name),
subtitle: Text(
device.vendorId == null
? 'USB printer'
: 'VID ${device.vendorId} • '
'PID ${device.productId}',
),
trailing: connected
? const Text('Connected')
: const Icon(Icons.chevron_right_rounded),
onTap: _busy ? null : () => _connect(device),
);
},
),
),
if (_connectedPrinter != null)
Padding(
padding: const EdgeInsets.all(12),
child: Row(
children: <Widget>[
Expanded(
child: FilledButton.icon(
onPressed: _busy ? null : _printTest,
icon: const Icon(Icons.print_rounded),
label: const Text('Test print'),
),
),
const SizedBox(width: 8),
Expanded(
child: OutlinedButton.icon(
onPressed: _busy ? null : _disconnect,
icon: const Icon(Icons.link_off_rounded),
label: const Text('Disconnect'),
),
),
],
),
),
],
),
),
);
}
}