flutter_xprinter_sdk 0.2.0 copy "flutter_xprinter_sdk: ^0.2.0" to clipboard
flutter_xprinter_sdk: ^0.2.0 copied to clipboard

Flutter plugin for XPrinter thermal receipt printers. Cyrillic-safe text, Floyd-Steinberg image dithering, BLE / Classic Bluetooth / USB / TCP transports.

example/lib/main.dart

import 'dart:io';
import 'dart:typed_data';

import 'package:flutter/material.dart';
import 'package:flutter_xprinter_sdk/flutter_xprinter_sdk.dart';
import 'package:permission_handler/permission_handler.dart';

void main() => runApp(const ExampleApp());

class ExampleApp extends StatelessWidget {
  const ExampleApp({super.key});

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'flutter_xprinter_sdk demo',
      theme: ThemeData(useMaterial3: true, colorSchemeSeed: Colors.indigo),
      home: const HomePage(),
    );
  }
}

class HomePage extends StatefulWidget {
  const HomePage({super.key});

  @override
  State<HomePage> createState() => _HomePageState();
}

class _HomePageState extends State<HomePage> {
  List<XprinterBluetoothDevice> _devices = const [];
  XprinterBluetoothDevice? _selected;
  final TextEditingController _windowsAddressController =
      TextEditingController(text: '192.168.1.100');
  XprinterConnectionType _windowsConnectionType = XprinterConnectionType.tcp;
  bool _scanning = false;
  bool _printing = false;
  int _paperSizeMm = 58;
  String _status = 'Idle';

  bool get _isWindows => Platform.isWindows;

  @override
  void dispose() {
    _windowsAddressController.dispose();
    super.dispose();
  }

  /// Combines `getBondedDevices` (Android Classic-BT paired devices —
  /// returns nothing on iOS) with a 12-second active scan. Android uses
  /// classic Bluetooth inquiry; iOS uses Core Bluetooth BLE scanning.
  /// Devices are deduped by Bluetooth address.
  Future<void> _scan() async {
    setState(() {
      _scanning = true;
      _status = 'Requesting permissions…';
    });
    try {
      if (!await _requestBluetoothPermissions()) {
        setState(() => _status = 'Bluetooth scan permission denied');
        return;
      }

      final byAddress = <String, XprinterBluetoothDevice>{};

      // 1. Bonded devices — instant, populates Android paired list.
      setState(() => _status = 'Loading paired devices…');
      try {
        for (final d in await XprinterBluetooth.getBondedDevices()) {
          byAddress[d.address] = d;
        }
      } catch (_) {/* ignore — bonded list isn't supported on iOS */}

      // 2. Active scan — finds nearby devices. iOS uses Core Bluetooth
      //    BLE scanning, Android uses classic Bluetooth inquiry.
      setState(() => _status = 'Scanning for nearby devices…');
      final stream = XprinterBluetooth.startDiscovery(
          timeout: const Duration(seconds: 12));
      await for (final d in stream) {
        byAddress[d.address] = d;
        setState(() {
          _devices = byAddress.values.toList();
          _selected ??= _devices.first;
        });
      }

      setState(() {
        _devices = byAddress.values.toList();
        _selected ??= _devices.isNotEmpty ? _devices.first : null;
        _status = _devices.isEmpty
            ? 'Found 0 devices. Make sure the printer is powered on; on Android, pair Classic Bluetooth printers in system Settings first.'
            : 'Found ${_devices.length} device(s)';
      });
    } catch (e) {
      setState(() => _status = 'Scan failed: $e');
    } finally {
      setState(() => _scanning = false);
    }
  }

  /// Requests runtime permissions needed for Bluetooth discovery.
  /// iOS handles this through `Info.plist` so this is a no-op there.
  Future<bool> _requestBluetoothPermissions() async {
    if (!Platform.isAndroid) return true;

    final bluetoothResults = await [
      Permission.bluetoothScan,
      Permission.bluetoothConnect,
    ].request();

    // Android 11 and older require location permission for Bluetooth inquiry
    // results. Android 12+ does not need it here because BLUETOOTH_SCAN is
    // declared with neverForLocation, so we ask but do not block on it.
    await Permission.locationWhenInUse.request();

    return bluetoothResults.values.every((s) => s.isGranted);
  }

  Future<void> _printDemoReceipt() async {
    final device = _selected;
    if (!_isWindows && device == null) {
      setState(() => _status = 'Pick a device first');
      return;
    }

    final connectionType =
        _isWindows ? _windowsConnectionType : XprinterConnectionType.bluetooth;
    final address =
        _isWindows ? _windowsAddressController.text.trim() : device!.address;
    if (connectionType == XprinterConnectionType.tcp && address.isEmpty) {
      setState(() => _status = 'Enter the printer IP address');
      return;
    }

    setState(() {
      _printing = true;
      _status = 'Connecting via ${connectionType.name}…';
    });

    try {
      await XprinterConnection.connect(
        type: connectionType,
        address: address,
      );

      await PosPrinter.initialize();
      // FS . — cancel multi-byte (Chinese) mode on Chinese-market models.
      await PosPrinter.sendRawCommand(Uint8List.fromList(<int>[0x1C, 0x2E]));
      await PosPrinter.selectCodePage(XprinterCodePage.pc866);
      XprinterLayout.configure(paperSizeMm: _paperSizeMm);

      setState(() => _status = 'Printing…');

      // ── 1. Logo from a Flutter asset (centred, top of receipt) ────────
      // Bundled in `assets/`.  Threshold-mode binarisation produces
      // crisp edges for line-art logos.
      final logoBytes = await XprinterImageLoader.fromAsset(
        assetPath: 'assets/dash-logo.png',
        targetWidthDots: XprinterLayout.widthDots,
        mode: XprinterImageMode.threshold,
      );
      if (logoBytes != null) {
        await PosPrinter.printBitmap(
          logoBytes,
          alignment: XprinterAlignment.center,
          widthDots: XprinterLayout.widthDots,
        );
        await PosPrinter.feedLine(1);
      }

      // ── 2. Shop header (centred, name in bold) ────────────────────────
      await XprinterLayout.printLine(
        'МАГАЗИН "АЛЬФА"',
        alignment: XprinterAlignment.center,
        bold: true,
      );
      await XprinterLayout.printLine(
        'ул. Чехова, 12, Ташкент',
        alignment: XprinterAlignment.center,
      );
      await XprinterLayout.printLine(
        'Тел: +998 90 123-45-67',
        alignment: XprinterAlignment.center,
      );
      await PosPrinter.feedLine(1);
      await XprinterLayout.printLine(
        'КАССОВЫЙ ЧЕК',
        alignment: XprinterAlignment.center,
        bold: true,
      );

      // ── 3. Date row, between two dotted dividers ──────────────────────
      await XprinterLayout.printSectionDivider(
        style: XprinterDividerStyle.dotted,
      );
      await XprinterLayout.printValueRow(
        'Дата: 07.05.2026',
        '14:32:18',
        leader: '',
      );
      await XprinterLayout.printSectionDivider(
        style: XprinterDividerStyle.dotted,
      );

      // ── 4. Items (label + price, no leader) ───────────────────────────
      await XprinterLayout.printValueRow(
        'Bonaqua 0,5 л',
        '8 000 сум',
        leader: '',
      );
      await XprinterLayout.printValueRow(
        'Хлеб 400 г x 2',
        '24 000 сум',
        leader: '',
      );
      await XprinterLayout.printValueRow(
        'Кефир 1 л',
        '15 000 сум',
        leader: '',
      );
      await XprinterLayout.printValueRow(
        'Скидка 10%',
        '-4 700 сум',
        leader: '',
      );
      await XprinterLayout.printSectionDivider(
        style: XprinterDividerStyle.dotted,
      );

      // ── 5. Totals (ИТОГО bold) ────────────────────────────────────────
      await XprinterLayout.printBoldRow(
        'ИТОГО',
        '42 300 сум',
        leader: '',
      );
      await XprinterLayout.printValueRow(
        'Наличные',
        '50 000 сум',
        leader: '',
      );
      await XprinterLayout.printValueRow(
        'Сдача',
        '7 700 сум',
        leader: '',
      );
      await XprinterLayout.printSectionDivider(
        style: XprinterDividerStyle.dotted,
      );

      // ── 6. Photo from a Flutter asset (centred, smaller width) ────────
      // Floyd-Steinberg dither preserves the photo's tonal gradients on
      // thermal paper — much better than the threshold path for non-line-art.
      final photoBytes = await XprinterImageLoader.fromAsset(
        assetPath: 'assets/dash.jpg',
        targetWidthDots: XprinterLayout.widthDots ~/ 2,
        mode: XprinterImageMode.dither,
      );
      if (photoBytes != null) {
        await PosPrinter.printBitmap(
          photoBytes,
          alignment: XprinterAlignment.center,
          widthDots: XprinterLayout.widthDots ~/ 2,
        );
        await PosPrinter.feedLine(1);
      }

      // ── 7. Footer ─────────────────────────────────────────────────────
      await XprinterLayout.printLine(
        'СПАСИБО ЗА ПОКУПКУ!',
        alignment: XprinterAlignment.center,
        bold: true,
      );
      await XprinterLayout.printLine(
        'Приходите ещё',
        alignment: XprinterAlignment.center,
      );
      await PosPrinter.feedLine(1);

      // ── 8. QR code (e.g. link to a digital copy of this receipt) ──────
      await PosPrinter.printQRCode(
        'https://github.com/Lazizbek97/flutter_xprinter_sdk',
        moduleSize: 6,
      );
      await PosPrinter.feedLine(1);

      // ── 9. Barcode (e.g. receipt number) ──────────────────────────────
      await PosPrinter.printBarCode(
        '1234567890128',
        type: XprinterBarcodeType.ean13,
        height: 60,
      );

      // ── 10. Cut & finish ──────────────────────────────────────────────
      await PosPrinter.feedLine(3);
      await PosPrinter.cutPaper();

      // Buffer drain before disconnect so the receipt tail isn't truncated.
      await Future<void>.delayed(const Duration(seconds: 2));

      setState(() => _status = 'Done');
    } catch (e) {
      setState(() => _status = 'Print failed: $e');
    } finally {
      try {
        await XprinterConnection.disconnect();
      } catch (_) {}
      setState(() => _printing = false);
    }
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: const Text('flutter_xprinter_sdk demo')),
      body: SafeArea(
        child: Padding(
          padding: const EdgeInsets.all(16),
          child: Column(
            crossAxisAlignment: CrossAxisAlignment.stretch,
            children: [
              Row(
                children: [
                  const Text('Paper size: '),
                  for (final mm in const [58, 72, 80])
                    Padding(
                      padding: const EdgeInsets.symmetric(horizontal: 4),
                      child: ChoiceChip(
                        label: Text('${mm}mm'),
                        selected: _paperSizeMm == mm,
                        onSelected: (_) => setState(() => _paperSizeMm = mm),
                      ),
                    ),
                ],
              ),
              const SizedBox(height: 12),
              if (_isWindows) ...[
                _buildWindowsConnectionForm(),
                const Spacer(),
              ] else ...[
                FilledButton.icon(
                  icon: _scanning
                      ? const SizedBox(
                          width: 16,
                          height: 16,
                          child: CircularProgressIndicator(strokeWidth: 2),
                        )
                      : const Icon(Icons.bluetooth_searching),
                  label: Text(_scanning ? 'Scanning…' : 'Scan for printers'),
                  onPressed: _scanning ? null : _scan,
                ),
                const SizedBox(height: 12),
                Expanded(
                  child: _devices.isEmpty
                      ? const Center(child: Text('No devices yet — tap Scan'))
                      : RadioGroup<XprinterBluetoothDevice>(
                          groupValue: _selected,
                          onChanged: (v) => setState(() => _selected = v),
                          child: ListView.separated(
                            itemCount: _devices.length,
                            separatorBuilder: (_, __) =>
                                const Divider(height: 1),
                            itemBuilder: (_, i) {
                              final d = _devices[i];
                              return RadioListTile<XprinterBluetoothDevice>(
                                title: Text(d.name),
                                subtitle: Text(d.address),
                                value: d,
                              );
                            },
                          ),
                        ),
                ),
              ],
              FilledButton.icon(
                icon: _printing
                    ? const SizedBox(
                        width: 16,
                        height: 16,
                        child: CircularProgressIndicator(strokeWidth: 2),
                      )
                    : const Icon(Icons.print),
                label: Text(_printing ? 'Printing…' : 'Print demo receipt'),
                onPressed: (_printing || (!_isWindows && _selected == null))
                    ? null
                    : _printDemoReceipt,
              ),
              const SizedBox(height: 8),
              Container(
                padding: const EdgeInsets.all(8),
                decoration: BoxDecoration(
                  color: Colors.black.withValues(alpha: 0.05),
                  borderRadius: BorderRadius.circular(6),
                ),
                // text should be copyable to clipboard for debugging, so no SelectableText here.

                child: SelectableText(
                  _status,
                  textAlign: TextAlign.center,
                  style: const TextStyle(fontSize: 12, fontFamily: 'monospace'),
                ),
              ),
            ],
          ),
        ),
      ),
    );
  }

  Widget _buildWindowsConnectionForm() {
    final isUsb = _windowsConnectionType == XprinterConnectionType.usb;
    return Card(
      child: Padding(
        padding: const EdgeInsets.all(16),
        child: Column(
          crossAxisAlignment: CrossAxisAlignment.stretch,
          children: [
            Text(
              'Windows connection',
              style: Theme.of(context).textTheme.titleMedium,
            ),
            const SizedBox(height: 12),
            SegmentedButton<XprinterConnectionType>(
              segments: const [
                ButtonSegment(
                  value: XprinterConnectionType.usb,
                  icon: Icon(Icons.usb),
                  label: Text('USB'),
                ),
                ButtonSegment(
                  value: XprinterConnectionType.tcp,
                  icon: Icon(Icons.lan),
                  label: Text('TCP/IP'),
                ),
              ],
              selected: {_windowsConnectionType},
              onSelectionChanged: (selection) {
                setState(() {
                  _windowsConnectionType = selection.single;
                  _windowsAddressController.text =
                      _windowsConnectionType == XprinterConnectionType.usb
                          ? ''
                          : '192.168.1.100';
                });
              },
            ),
            const SizedBox(height: 12),
            TextField(
              controller: _windowsAddressController,
              decoration: InputDecoration(
                border: const OutlineInputBorder(),
                labelText: isUsb
                    ? 'USB model / port (optional)'
                    : 'Printer IP address',
                hintText: isUsb ? 'Empty = first USB printer' : '192.168.1.100',
                helperText: isUsb
                    ? 'Examples: 4B-2054A or USB031'
                    : 'Optional port: 192.168.1.100:9100',
              ),
            ),
            const SizedBox(height: 12),
            const Text(
              'The Windows SDK supports USB and TCP/IP. '
              'Bluetooth is not available on Windows.',
            ),
          ],
        ),
      ),
    );
  }
}
4
likes
150
points
123
downloads

Documentation

API reference

Publisher

unverified uploader

Weekly Downloads

Flutter plugin for XPrinter thermal receipt printers. Cyrillic-safe text, Floyd-Steinberg image dithering, BLE / Classic Bluetooth / USB / TCP transports.

Repository (GitHub)
View/report issues

Topics

#printer #thermal #esc-pos #xprinter #bluetooth

License

MIT (license)

Dependencies

flutter, http, image, plugin_platform_interface

More

Packages that depend on flutter_xprinter_sdk

Packages that implement flutter_xprinter_sdk