Skip to main content

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

PlatformAndroid

Direct Android printing for XPrinter label and POS printers over Bluetooth, TCP/IP, and USB with TSPL, ZPL, CPCL, and ESC/POS support.

example/lib/main.dart

import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:xprinter_flutter/xprinter_flutter.dart';

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

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

  @override
  State<ExampleApp> createState() => _ExampleAppState();
}

class _ExampleAppState extends State<ExampleApp> {
  final printer = XPrinterFlutter.instance;
  final messengerKey = GlobalKey<ScaffoldMessengerState>();

  final widthController = TextEditingController(text: '60');
  final heightController = TextEditingController(text: '40');
  final gapController = TextEditingController(text: '2');

  List<XPrinterDevice> devices = const [];
  String status = 'Disconnected';
  bool busy = false;

  void showMessage(String message) {
    messengerKey.currentState?.showSnackBar(
      SnackBar(content: Text(message)),
    );
  }

  double readPositiveMm(
    TextEditingController controller,
    String fieldName,
  ) {
    final value = double.tryParse(controller.text.trim());
    if (value == null || value <= 0) {
      throw FormatException('$fieldName must be greater than 0 mm');
    }
    return value;
  }

  double readGapMm() {
    final value = double.tryParse(gapController.text.trim());
    if (value == null || value < 0) {
      throw const FormatException('Gap must be 0 mm or greater');
    }
    return value;
  }

  String formatMm(double value) {
    if (value == value.roundToDouble()) {
      return value.toInt().toString();
    }
    return value.toString();
  }

  Future<void> loadPrinters() async {
    try {
      setState(() => busy = true);

      final granted = await printer.requestBluetoothPermissions();
      if (!granted) {
        setState(() => status = 'Bluetooth permission denied');
        return;
      }

      final result = await printer.getBondedBluetoothDevices();

      if (!mounted) return;
      setState(() {
        devices = result;
        status = result.isEmpty
            ? 'No paired printers. Pair XP-365B in Android Bluetooth settings first.'
            : 'Select XP-365B and tap Connect';
      });
    } on PlatformException catch (e) {
      setState(() => status = 'Error: ${e.message ?? e.code}');
      showMessage(status);
    } catch (e) {
      setState(() => status = 'Error: $e');
      showMessage(status);
    } finally {
      if (mounted) setState(() => busy = false);
    }
  }

  Future<void> connect(XPrinterDevice device) async {
    try {
      setState(() {
        busy = true;
        status = 'Connecting to ${device.name}...';
      });

      await printer.connectBluetooth(device.address);

      final connected = await printer.isConnected();

      if (!mounted) return;
      setState(() {
        status = connected
            ? 'Connected: ${device.name} (${device.address})'
            : 'Connection failed: ${device.name}';
      });

      if (connected) {
        showMessage('XP-365B connected successfully');
      }
    } on PlatformException catch (e) {
      if (!mounted) return;
      setState(() => status = 'Connect error: ${e.message ?? e.code}');
      showMessage(status);
    } catch (e) {
      if (!mounted) return;
      setState(() => status = 'Connect error: $e');
      showMessage(status);
    } finally {
      if (mounted) setState(() => busy = false);
    }
  }

  Future<bool> ensureConnected() async {
    final connected = await printer.isConnected();
    if (!connected) {
      if (!mounted) return false;
      setState(() => status = 'Printer is not connected');
      showMessage('Connect XP-365B first');
    }
    return connected;
  }

  Future<void> calibrateGapSensor() async {
    if (!await ensureConnected()) return;

    try {
      setState(() {
        busy = true;
        status = 'Calibrating gap sensor...';
      });

      await printer.calibrateGapSensor();

      if (!mounted) return;
      setState(() => status = 'Gap sensor calibration command sent');
      showMessage(
        'Calibration sent. Let the printer finish feeding labels before printing.',
      );
    } on PlatformException catch (e) {
      if (!mounted) return;
      setState(() => status = 'Calibration error: ${e.message ?? e.code}');
      showMessage(status);
    } catch (e) {
      if (!mounted) return;
      setState(() => status = 'Calibration error: $e');
      showMessage(status);
    } finally {
      if (mounted) setState(() => busy = false);
    }
  }

  Future<void> printContinuousMediaTest() async {
    if (!await ensureConnected()) return;

    try {
      final width = readPositiveMm(widthController, 'Label width');
      final height = readPositiveMm(heightController, 'Label height');

      setState(() {
        busy = true;
        status = 'TEST: printing with gap sensor disabled...';
      });

      await printer.printTsplRaw(
        'SIZE ${formatMm(width)} mm,${formatMm(height)} mm\r\n'
        'GAP 0,0\r\n'
        'SPEED 2\r\n'
        'DENSITY 6\r\n'
        'DIRECTION 1,0\r\n'
        'REFERENCE 0,0\r\n'
        'CLS\r\n'
        'TEXT 20,20,"3",0,1,1,"CONTINUOUS TEST"\r\n'
        'TEXT 20,60,"3",0,1,1,"GAP SENSOR OFF"\r\n'
        'PRINT 1,1\r\n',
      );

      if (!mounted) return;
      setState(() => status = 'Continuous-media test sent');
      showMessage(
        'Test sent. If ERROR light stays OFF, the gap/media sensor path is the problem.',
      );
    } on FormatException catch (e) {
      if (!mounted) return;
      setState(() => status = 'Media setting error: ${e.message}');
      showMessage(status);
    } on PlatformException catch (e) {
      if (!mounted) return;
      setState(() => status = 'Continuous test error: ${e.message ?? e.code}');
      showMessage(status);
    } catch (e) {
      if (!mounted) return;
      setState(() => status = 'Continuous test error: $e');
      showMessage(status);
    } finally {
      if (mounted) setState(() => busy = false);
    }
  }

  Future<void> printStoredMediaTest() async {
    if (!await ensureConnected()) return;

    try {
      setState(() {
        busy = true;
        status = 'TEST: printing with stored printer settings...';
      });

      await printer.printTsplRaw(
        'CLS\r\n'
        'TEXT 20,20,"3",0,1,1,"STORED MEDIA TEST"\r\n'
        'PRINT 1,1\r\n',
      );

      if (!mounted) return;
      setState(() => status = 'Stored-media test sent');
      showMessage(
        'Test sent without SIZE/GAP commands. This uses the printer\'s saved media settings.',
      );
    } on PlatformException catch (e) {
      if (!mounted) return;
      setState(() => status = 'Stored-media test error: ${e.message ?? e.code}');
      showMessage(status);
    } catch (e) {
      if (!mounted) return;
      setState(() => status = 'Stored-media test error: $e');
      showMessage(status);
    } finally {
      if (mounted) setState(() => busy = false);
    }
  }

  Future<void> feedOneLabelTest() async {
    if (!await ensureConnected()) return;

    try {
      setState(() {
        busy = true;
        status = 'TEST: feeding to next label...';
      });

      await printer.printTsplRaw('FORMFEED\r\n');

      if (!mounted) return;
      setState(() => status = 'FORMFEED test sent');
      showMessage(
        'If FORMFEED alone turns the ERROR light on, the problem is media/sensor related.',
      );
    } on PlatformException catch (e) {
      if (!mounted) return;
      setState(() => status = 'Feed test error: ${e.message ?? e.code}');
      showMessage(status);
    } catch (e) {
      if (!mounted) return;
      setState(() => status = 'Feed test error: $e');
      showMessage(status);
    } finally {
      if (mounted) setState(() => busy = false);
    }
  }

  Future<void> printTsplDemo() async {
    if (!await ensureConnected()) return;

    try {
      setState(() {
        busy = true;
        status = 'Sending SAFE XP-365B TSPL label...';
      });

      await printer.printTsplLabel(
        const TsplLabel(
          // Required by the generic label model, but intentionally NOT sent
          // to the printer when useStoredPrinterSettings is true.
          widthMm: 60,
          heightMm: 40,
          useStoredPrinterSettings: true,
          elements: [
            TsplText(
              x: 20,
              y: 20,
              text: 'XP-365B SAFE TEST',
            ),
            TsplText(
              x: 20,
              y: 60,
              text: 'STORED SETTINGS',
            ),
          ],
        ),
      );

      if (!mounted) return;
      setState(() => status = 'SAFE TSPL print sent using stored printer settings');
      showMessage(
        'Safe print sent. No SIZE/GAP/SPEED/DENSITY/DIRECTION/REFERENCE commands were sent.',
      );
    } on PlatformException catch (e) {
      if (!mounted) return;
      setState(() => status = 'SAFE TSPL error: ${e.message ?? e.code}');
      showMessage(status);
    } catch (e) {
      if (!mounted) return;
      setState(() => status = 'SAFE TSPL error: $e');
      showMessage(status);
    } finally {
      if (mounted) setState(() => busy = false);
    }
  }

  Future<void> printEscPosDemo() async {
    if (!await ensureConnected()) return;

    try {
      setState(() {
        busy = true;
        status = 'Sending ESC/POS receipt test...';
      });

      await printer.printPosText(
        'XP-365B ESC/POS TEST\n'
        'xprinter_flutter\n'
        '1234567890\n',
        feedLines: 3,
      );

      if (!mounted) return;
      setState(() => status = 'ESC/POS data sent successfully');
      showMessage(
        'ESC/POS sent. If this prints but TSPL does not, printer is in RECEIPT mode.',
      );
    } on PlatformException catch (e) {
      if (!mounted) return;
      setState(() => status = 'ESC/POS error: ${e.message ?? e.code}');
      showMessage(status);
    } catch (e) {
      if (!mounted) return;
      setState(() => status = 'ESC/POS error: $e');
      showMessage(status);
    } finally {
      if (mounted) setState(() => busy = false);
    }
  }

  Future<void> disconnect() async {
    await printer.disconnect();
    if (!mounted) return;
    setState(() => status = 'Disconnected');
  }

  Widget mediaField(
    String label,
    TextEditingController controller,
  ) {
    return Expanded(
      child: TextField(
        controller: controller,
        enabled: !busy,
        keyboardType: const TextInputType.numberWithOptions(decimal: true),
        decoration: InputDecoration(
          labelText: label,
          suffixText: 'mm',
          border: const OutlineInputBorder(),
        ),
      ),
    );
  }

  @override
  void initState() {
    super.initState();
    loadPrinters();
  }

  @override
  void dispose() {
    widthController.dispose();
    heightController.dispose();
    gapController.dispose();
    super.dispose();
  }

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      scaffoldMessengerKey: messengerKey,
      home: Scaffold(
        appBar: AppBar(
          title: const Text('xprinter_flutter - XP-365B test'),
        ),
        body: ListView(
          padding: const EdgeInsets.all(16),
          children: [
            Card(
              child: Padding(
                padding: const EdgeInsets.all(16),
                child: Text(
                  status,
                  style: const TextStyle(fontWeight: FontWeight.w600),
                ),
              ),
            ),
            const SizedBox(height: 12),
            FilledButton(
              onPressed: busy ? null : loadPrinters,
              child: const Text('Refresh paired printers'),
            ),
            const SizedBox(height: 8),
            if (devices.isEmpty)
              const Padding(
                padding: EdgeInsets.all(12),
                child: Text(
                  'XP-365B not shown? Pair it first from Android Settings > Bluetooth.',
                ),
              ),
            ...devices.map(
              (device) => Card(
                child: ListTile(
                  title: Text(device.name),
                  subtitle: Text(device.address),
                  trailing: FilledButton.tonal(
                    onPressed: busy ? null : () => connect(device),
                    child: const Text('Connect'),
                  ),
                ),
              ),
            ),
            const Divider(height: 32),
            Card(
              child: Padding(
                padding: const EdgeInsets.all(16),
                child: Column(
                  crossAxisAlignment: CrossAxisAlignment.start,
                  children: [
                    const Text(
                      'Label media',
                      style: TextStyle(
                        fontSize: 16,
                        fontWeight: FontWeight.w700,
                      ),
                    ),
                    const SizedBox(height: 6),
                    const Text(
                      'Enter the real sticker width, height and physical gap. '
                      'Wrong media values can make the XP-365B stop with the ERROR light on.',
                    ),
                    const SizedBox(height: 14),
                    Row(
                      children: [
                        mediaField('Width', widthController),
                        const SizedBox(width: 8),
                        mediaField('Height', heightController),
                        const SizedBox(width: 8),
                        mediaField('Gap', gapController),
                      ],
                    ),
                    const SizedBox(height: 12),
                    OutlinedButton.icon(
                      onPressed: busy ? null : calibrateGapSensor,
                      icon: const Icon(Icons.tune),
                      label: const Text('Calibrate gap sensor'),
                    ),
                    const SizedBox(height: 6),
                    const Text(
                      'Run calibration after changing the label roll or if the ERROR light appears after a print. '
                      'Calibration feeds labels while the sensor detects the media.',
                    ),
                  ],
                ),
              ),
            ),
            const SizedBox(height: 12),
            Card(
              child: Padding(
                padding: const EdgeInsets.all(16),
                child: Column(
                  crossAxisAlignment: CrossAxisAlignment.stretch,
                  children: [
                    const Text(
                      'ERROR LIGHT DIAGNOSTIC',
                      style: TextStyle(
                        fontSize: 16,
                        fontWeight: FontWeight.w800,
                      ),
                    ),
                    const SizedBox(height: 6),
                    const Text(
                      'Restart the printer first. Then run these tests ONE BY ONE. '
                      'Do not calibrate before Test 1.',
                    ),
                    const SizedBox(height: 12),
                    FilledButton.icon(
                      onPressed: busy ? null : printContinuousMediaTest,
                      icon: const Icon(Icons.science_outlined),
                      label: const Text('TEST 1 - Print with GAP SENSOR OFF'),
                    ),
                    const SizedBox(height: 8),
                    OutlinedButton(
                      onPressed: busy ? null : printStoredMediaTest,
                      child: const Text('TEST 2 - Print with stored media settings'),
                    ),
                    const SizedBox(height: 8),
                    OutlinedButton(
                      onPressed: busy ? null : feedOneLabelTest,
                      child: const Text('TEST 3 - FORMFEED only'),
                    ),
                    const SizedBox(height: 12),
                    const Text(
                      'Result guide:\n'
                      '• Test 1 OK, normal TSPL gives ERROR = gap sensor/media detection issue.\n'
                      '• Test 1 also gives ERROR = not just the gap setting.\n'
                      '• Test 2 gives ERROR = printer saved media setting/sensor state is bad.\n'
                      '• Test 3 alone gives ERROR = media sensor cannot find the next label.\n'
                      '• Your current result (Test 1 ERROR, Test 2/3 OK) means stored-settings mode is the safe path.',
                    ),
                  ],
                ),
              ),
            ),
            const SizedBox(height: 12),
            FilledButton(
              onPressed: busy ? null : printTsplDemo,
              child: const Text('SAFE XP-365B print (stored settings)'),
            ),
            const SizedBox(height: 10),
            OutlinedButton(
              onPressed: busy ? null : printEscPosDemo,
              child: const Text('Print ESC/POS receipt test'),
            ),
            const SizedBox(height: 10),
            TextButton(
              onPressed: busy ? null : disconnect,
              child: const Text('Disconnect'),
            ),
            const SizedBox(height: 20),
            const Text(
              'Diagnosis:\n'
              '1. Connect XP-365B first.\n'
              '2. Enter the actual label size and gap.\n'
              '3. Run Calibrate gap sensor and wait until feeding stops.\n'
              '4. Print the TSPL label test.\n'
              '5. If ESC/POS prints but TSPL does not, verify that the printer is in LABEL mode.',
            ),
          ],
        ),
      ),
    );
  }
}
1
likes
140
points
559
downloads

Documentation

API reference

Publisher

unverified uploader

Weekly Downloads

Direct Android printing for XPrinter label and POS printers over Bluetooth, TCP/IP, and USB with TSPL, ZPL, CPCL, and ESC/POS support.

Repository (GitHub)
View/report issues

License

unknown (license)

Dependencies

flutter, printing

More

Packages that depend on xprinter_flutter

Packages that implement xprinter_flutter