brother_print_library 0.1.7
brother_print_library: ^0.1.7 copied to clipboard
Flutter Android plugin wrapping BrotherPrintLibrary.aar for Brother mobile printer discovery, connection, status, and print commands.
example/lib/main.dart
import 'dart:typed_data';
import 'dart:ui' as ui;
import 'package:brother_print_library/brother_print_library.dart';
import 'package:flutter/material.dart';
const _defaultQlLabelSize = 'DieCutW62H29';
const _qlLabelSizes = <String>[
'DieCutW17H54',
'DieCutW17H87',
'DieCutW23H23',
'DieCutW29H42',
'DieCutW29H90',
'DieCutW38H90',
'DieCutW39H48',
'DieCutW52H29',
'DieCutW62H29',
'DieCutW62H60',
'DieCutW62H75',
'DieCutW62H100',
'DieCutW60H86',
'DieCutW54H29',
'DieCutW102H51',
'DieCutW102H152',
'DieCutW103H164',
'RollW12',
'RollW29',
'RollW38',
'RollW50',
'RollW54',
'RollW62',
'RollW62RB',
'RollW102',
'RollW103',
'DTRollW90',
'DTRollW102',
'DTRollW102H51',
'DTRollW102H152',
'RoundW12DIA',
'RoundW24DIA',
'RoundW58DIA',
];
void main() {
runApp(const BrotherPrintExampleApp());
}
class BrotherPrintExampleApp extends StatelessWidget {
const BrotherPrintExampleApp({super.key});
@override
Widget build(BuildContext context) {
return MaterialApp(
theme: ThemeData(
colorScheme: ColorScheme.fromSeed(seedColor: const Color(0xff1769aa)),
useMaterial3: true,
),
home: const PrinterDemoShell(),
);
}
}
class PrinterDemoShell extends StatefulWidget {
const PrinterDemoShell({super.key});
@override
State<PrinterDemoShell> createState() => _PrinterDemoShellState();
}
class _PrinterDemoShellState extends State<PrinterDemoShell> {
static const _titles = <String>['Wi-Fi Demo', 'QL-820NWB USB Demo'];
int _selectedIndex = 0;
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: Text(_titles[_selectedIndex])),
body: IndexedStack(
index: _selectedIndex,
children: const <Widget>[WifiPrinterDemoPage(), UsbQl820DemoPage()],
),
bottomNavigationBar: NavigationBar(
selectedIndex: _selectedIndex,
onDestinationSelected: (index) {
setState(() => _selectedIndex = index);
},
destinations: const <NavigationDestination>[
NavigationDestination(icon: Icon(Icons.wifi), label: 'Wi-Fi'),
NavigationDestination(icon: Icon(Icons.usb), label: 'USB'),
],
),
);
}
}
class WifiPrinterDemoPage extends StatefulWidget {
const WifiPrinterDemoPage({super.key});
@override
State<WifiPrinterDemoPage> createState() => _WifiPrinterDemoPageState();
}
class _WifiPrinterDemoPageState extends State<WifiPrinterDemoPage> {
final _brother = BrotherPrintLibrary();
final _ipController = TextEditingController(text: '192.168.1.100');
final _modelController = TextEditingController(text: 'QL_820NWB');
final _pathController = TextEditingController();
final _channels = <BrotherChannel>[];
String _status = 'Idle';
String _selectedLabelSize = _defaultQlLabelSize;
bool _busy = false;
@override
void dispose() {
_ipController.dispose();
_modelController.dispose();
_pathController.dispose();
super.dispose();
}
Future<void> _run(String label, Future<String> Function() action) async {
setState(() {
_busy = true;
_status = '$label...';
});
try {
final message = await action();
if (!mounted) return;
setState(() => _status = message);
} catch (error) {
if (!mounted) return;
setState(() => _status = error.toString());
} finally {
if (mounted) {
setState(() => _busy = false);
}
}
}
Future<void> _searchNetwork() {
return _run('Searching network', () async {
final result = await _brother.searchNetwork(durationSeconds: 5);
_channels
..clear()
..addAll(result.channels);
return result.success
? 'Found ${result.channels.length} network printer(s)'
: 'Search failed: ${result.error?.code ?? 'unknown'}';
});
}
Future<void> _openWifi() {
return _run('Opening Wi-Fi channel', () async {
final result = await _brother.openChannel(
BrotherChannel.wifi(_ipController.text.trim()),
);
return result.success
? 'Channel opened'
: 'Open failed: ${result.error?.code ?? 'unknown'}';
});
}
Future<void> _getStatus() {
return _run('Reading status', () async {
final result = await _brother.getPrinterStatus();
final status = result.status;
return result.success
? 'Model ${status?.model ?? '-'}, error ${status?.errorCode ?? '-'}'
: 'Status failed: ${result.error?.code ?? 'unknown'}';
});
}
Future<void> _printImage() {
return _run('Printing', () async {
final settings = BrotherPrintSettings(
printerModel: PrinterModel.fromName(_modelController.text),
settingsType: _settingsTypeFromModelName(_modelController.text),
options: <String, Object?>{
'labelSize': _selectedLabelSize,
'numCopies': 1,
'scaleMode': 'FitPaperAspect',
},
);
final imagePath = _pathController.text.trim();
final result = imagePath.isEmpty
? await _brother.printImageBytes(
await _buildDemoLabelPng(labelSize: _selectedLabelSize),
settings,
)
: await _brother.printImage(imagePath, settings);
return result.success
? 'Print command completed'
: 'Print failed: ${result.error?.description ?? result.error?.code ?? 'unknown'}';
});
}
@override
Widget build(BuildContext context) {
return ListView(
padding: const EdgeInsets.all(16),
children: [
TextField(
controller: _ipController,
decoration: const InputDecoration(
border: OutlineInputBorder(),
labelText: 'Wi-Fi address',
),
keyboardType: TextInputType.url,
),
const SizedBox(height: 12),
TextField(
controller: _modelController,
decoration: const InputDecoration(
border: OutlineInputBorder(),
labelText: 'Printer model',
),
),
const SizedBox(height: 12),
_LabelSizeDropdown(
value: _selectedLabelSize,
enabled: !_busy,
onChanged: (value) {
setState(() => _selectedLabelSize = value);
},
),
const SizedBox(height: 12),
TextField(
controller: _pathController,
decoration: const InputDecoration(
border: OutlineInputBorder(),
labelText: 'Image path',
),
),
const SizedBox(height: 16),
Wrap(
spacing: 8,
runSpacing: 8,
children: [
FilledButton.icon(
onPressed: _busy ? null : _searchNetwork,
icon: const Icon(Icons.search),
label: const Text('Search'),
),
FilledButton.icon(
onPressed: _busy ? null : _openWifi,
icon: const Icon(Icons.link),
label: const Text('Open'),
),
FilledButton.icon(
onPressed: _busy ? null : _getStatus,
icon: const Icon(Icons.info_outline),
label: const Text('Status'),
),
FilledButton.icon(
onPressed: _busy ? null : _printImage,
icon: const Icon(Icons.print),
label: const Text('Print'),
),
],
),
const SizedBox(height: 16),
LinearProgressIndicator(value: _busy ? null : 0),
const SizedBox(height: 16),
Text(_status),
const SizedBox(height: 16),
for (final channel in _channels)
ListTile(
contentPadding: EdgeInsets.zero,
title: Text(channel.extraInfo['ModelName'] ?? channel.type),
subtitle: Text(channel.channelInfo ?? '-'),
trailing: const Icon(Icons.chevron_right),
onTap: _busy
? null
: () {
_run('Opening', () async {
final result = await _brother.openChannel(channel);
return result.success
? 'Channel opened'
: 'Open failed: ${result.error?.code ?? 'unknown'}';
});
},
),
],
);
}
}
class UsbQl820DemoPage extends StatefulWidget {
const UsbQl820DemoPage({super.key});
@override
State<UsbQl820DemoPage> createState() => _UsbQl820DemoPageState();
}
class _UsbQl820DemoPageState extends State<UsbQl820DemoPage> {
final _brother = BrotherPrintLibrary();
final _channels = <BrotherChannel>[];
String _status = 'Ready';
bool _busy = false;
BrotherChannel? _selectedChannel;
BrotherPrinterStatus? _printerStatus;
String _selectedLabelSize = _defaultQlLabelSize;
BrotherPrintSettings get _settings {
return BrotherPrintSettings(
printerModel: PrinterModel.QL_820NWB,
settingsType: 'QL',
options: <String, Object?>{
'labelSize': _selectedLabelSize,
'numCopies': 1,
'autoCut': true,
'cutAtEnd': true,
'scaleMode': 'FitPaperAspect',
},
);
}
Future<void> _run(String label, Future<String> Function() action) async {
setState(() {
_busy = true;
_status = '$label...';
});
try {
final message = await action();
if (!mounted) return;
setState(() => _status = message);
} catch (error) {
if (!mounted) return;
setState(() => _status = error.toString());
} finally {
if (mounted) {
setState(() => _busy = false);
}
}
}
Future<void> _searchUsb() {
return _run('Searching USB', () async {
final result = await _brother.searchUsb();
_channels
..clear()
..addAll(result.channels);
if (!result.success) {
return 'USB search failed: ${result.error?.code ?? 'unknown'}';
}
if (result.channels.isEmpty) {
_selectedChannel = null;
return 'No USB printer found';
}
_selectedChannel = result.channels.first;
final extra = _selectedChannel!.extraInfo;
final modelName = extra['ModelName'] ?? _selectedChannel!.type;
return 'USB printer found: $modelName';
});
}
Future<void> _openUsb() {
return _run('Opening USB channel', () async {
final channel = _selectedChannel ?? BrotherChannel.usb();
final result = await _brother.openChannel(channel);
if (!result.success) {
return 'Open failed: ${result.error?.code ?? 'unknown'}';
}
_selectedChannel = result.channel ?? channel;
return 'USB channel opened';
});
}
Future<void> _readStatus() {
return _run('Reading status', () async {
final result = await _brother.getPrinterStatus();
_printerStatus = result.status;
if (!result.success) {
return 'Status failed: ${result.error?.code ?? 'unknown'}';
}
final media = _printerStatus?.mediaInfo?.qlLabelSize ?? '-';
final error = _printerStatus?.errorCode ?? '-';
return 'Status: $error, media: $media';
});
}
Future<void> _printDemoLabel() {
return _run('Printing $_selectedLabelSize', () async {
final open = await _brother.openChannel(
_selectedChannel ?? BrotherChannel.usb(),
);
if (!open.success) {
return 'Open failed: ${open.error?.code ?? 'unknown'}';
}
final bytes = await _buildDemoLabelPng(labelSize: _selectedLabelSize);
final result = await _brother.printImageBytes(bytes, _settings);
if (!result.success) {
return 'Print failed: ${result.error?.description ?? result.error?.code ?? 'unknown'}';
}
final status = await _brother.getPrinterStatus();
_printerStatus = status.status;
return 'Print command completed';
});
}
@override
Widget build(BuildContext context) {
final status = _printerStatus;
final mediaInfo = status?.mediaInfo;
return ListView(
padding: const EdgeInsets.all(16),
children: [
_LabelSizeDropdown(
value: _selectedLabelSize,
enabled: !_busy,
onChanged: (value) {
setState(() => _selectedLabelSize = value);
},
),
const SizedBox(height: 16),
Wrap(
spacing: 8,
runSpacing: 8,
children: [
FilledButton.icon(
onPressed: _busy ? null : _searchUsb,
icon: const Icon(Icons.usb),
label: const Text('Search USB'),
),
FilledButton.icon(
onPressed: _busy ? null : _openUsb,
icon: const Icon(Icons.link),
label: const Text('Open'),
),
FilledButton.icon(
onPressed: _busy ? null : _readStatus,
icon: const Icon(Icons.info_outline),
label: const Text('Status'),
),
FilledButton.icon(
onPressed: _busy ? null : _printDemoLabel,
icon: const Icon(Icons.print),
label: const Text('Test Print'),
),
],
),
const SizedBox(height: 16),
LinearProgressIndicator(value: _busy ? null : 0),
const SizedBox(height: 16),
Text(_status),
const SizedBox(height: 16),
ListTile(
contentPadding: EdgeInsets.zero,
title: const Text('Printer'),
subtitle: Text(
status?.model ?? _selectedChannel?.extraInfo['ModelName'] ?? '-',
),
),
ListTile(
contentPadding: EdgeInsets.zero,
title: const Text('Error'),
subtitle: Text(status?.errorCode ?? '-'),
),
ListTile(
contentPadding: EdgeInsets.zero,
title: const Text('Media'),
subtitle: Text(
[
mediaInfo?.qlLabelSize,
if (mediaInfo != null)
'${mediaInfo.widthMm}x${mediaInfo.heightMm}mm',
].whereType<String>().join(' | ').ifEmpty('-'),
),
),
const SizedBox(height: 8),
for (final channel in _channels)
ListTile(
contentPadding: EdgeInsets.zero,
title: Text(channel.extraInfo['ModelName'] ?? channel.type),
subtitle: Text(channel.channelInfo ?? '-'),
selected: identical(channel, _selectedChannel),
trailing: const Icon(Icons.chevron_right),
onTap: _busy
? null
: () {
setState(() => _selectedChannel = channel);
},
),
],
);
}
}
class _LabelSizeDropdown extends StatelessWidget {
const _LabelSizeDropdown({
required this.value,
required this.onChanged,
this.enabled = true,
});
final String value;
final ValueChanged<String> onChanged;
final bool enabled;
@override
Widget build(BuildContext context) {
return DropdownButtonFormField<String>(
value: value,
isExpanded: true,
menuMaxHeight: 360,
decoration: const InputDecoration(
border: OutlineInputBorder(),
labelText: 'QL label size',
),
items: _qlLabelSizes
.map(
(labelSize) => DropdownMenuItem<String>(
value: labelSize,
child: Text(labelSize, overflow: TextOverflow.ellipsis),
),
)
.toList(),
onChanged: enabled
? (value) {
if (value != null) {
onChanged(value);
}
}
: null,
);
}
}
String _settingsTypeFromModelName(String modelName) {
final normalized = modelName.trim().replaceAll('-', '_').replaceAll(' ', '_');
final separatorIndex = normalized.indexOf('_');
final type = separatorIndex == -1
? normalized
: normalized.substring(0, separatorIndex);
return type.toUpperCase();
}
Future<Uint8List> _buildDemoLabelPng({
String labelSize = _defaultQlLabelSize,
}) async {
const width = 732;
const height = 342;
final recorder = ui.PictureRecorder();
final labelRect = Rect.fromLTWH(0, 0, width.toDouble(), height.toDouble());
final canvas = Canvas(recorder, labelRect);
final backgroundPaint = Paint()..color = Colors.white;
canvas.drawRect(labelRect, backgroundPaint);
final borderPaint = Paint()
..color = Colors.black
..style = PaintingStyle.stroke
..strokeWidth = 6;
canvas.drawRRect(
RRect.fromRectAndRadius(
const Rect.fromLTWH(12, 12, width - 24, height - 24),
const Radius.circular(18),
),
borderPaint,
);
final linePaint = Paint()
..color = Colors.black
..strokeWidth = 4;
canvas.drawLine(
const Offset(36, 232),
const Offset(width - 36, 232),
linePaint,
);
_drawText(
canvas,
text: 'QL-820NWB',
rect: const Rect.fromLTWH(42, 42, width - 84, 72),
style: const TextStyle(
color: Colors.black,
fontSize: 58,
fontWeight: FontWeight.w800,
),
);
_drawText(
canvas,
text: labelSize,
rect: const Rect.fromLTWH(42, 126, width - 84, 58),
style: const TextStyle(
color: Colors.black,
fontSize: 40,
fontWeight: FontWeight.w600,
),
);
_drawText(
canvas,
text: DateTime.now().toIso8601String().substring(0, 19),
rect: const Rect.fromLTWH(42, 248, width - 84, 42),
style: const TextStyle(
color: Colors.black,
fontSize: 30,
fontWeight: FontWeight.w500,
),
);
final picture = recorder.endRecording();
final image = await picture.toImage(width, height);
final byteData = await image.toByteData(format: ui.ImageByteFormat.png);
picture.dispose();
image.dispose();
if (byteData == null) {
throw StateError('Failed to create demo label image');
}
return byteData.buffer.asUint8List();
}
void _drawText(
Canvas canvas, {
required String text,
required Rect rect,
required TextStyle style,
}) {
final painter = TextPainter(
text: TextSpan(text: text, style: style),
textDirection: TextDirection.ltr,
maxLines: 1,
)..layout(maxWidth: rect.width);
painter.paint(canvas, rect.topLeft);
}
extension _EmptyStringFallback on String {
String ifEmpty(String fallback) => isEmpty ? fallback : this;
}