brother_print_library 0.1.7 copy "brother_print_library: ^0.1.7" to clipboard
brother_print_library: ^0.1.7 copied to clipboard

PlatformAndroid

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;
}
0
likes
140
points
64
downloads

Documentation

API reference

Publisher

unverified uploader

Weekly Downloads

Flutter Android plugin wrapping BrotherPrintLibrary.aar for Brother mobile printer discovery, connection, status, and print commands.

Topics

#brother #printer #printing #android

License

MIT (license)

Dependencies

flutter

More

Packages that depend on brother_print_library

Packages that implement brother_print_library