niim_blue_flutter 1.0.1 copy "niim_blue_flutter: ^1.0.1" to clipboard
niim_blue_flutter: ^1.0.1 copied to clipboard

Flutter library for BLE printing with NIIMBOT thermal printers. Supports 85+ printer models with automatic detection and rich content rendering.

example/lib/main.dart

import 'dart:io';
import 'dart:typed_data';
import 'package:device_info_plus/device_info_plus.dart';
import 'package:flutter/material.dart';
import 'package:flutter_blue_plus/flutter_blue_plus.dart';
import 'package:niim_blue_flutter/niim_blue_flutter.dart';
import 'package:permission_handler/permission_handler.dart';

void main() {
  runApp(const MyApp());
}

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

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'NiimBlueLibRN',
      theme: ThemeData(
        useMaterial3: true,
        colorScheme: ColorScheme.fromSeed(seedColor: Colors.blue),
      ),
      home: const AppContent(),
    );
  }
}

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

  @override
  State<AppContent> createState() => _AppContentState();
}

class _AppContentState extends State<AppContent> {
  NiimbotBluetoothClient? _client;
  String _status = 'Disconnected';
  List<BluetoothDevice> _devices = [];
  bool _showDeviceList = false;
  Uint8List? _previewImage;
  bool _showPreview = false;
  Future<void> Function()? _pendingPrintAction;

  Future<bool> _requestPermissions() async {
    if (Platform.isIOS) {
      // iOS doesn't need runtime permissions
      // Bluetooth permissions are handled via Info.plist
      return true;
    }

    // Android 12+ (SDK 31+) needs bluetoothScan and bluetoothConnect
    // Older Android needs bluetooth and location
    final androidInfo = await DeviceInfoPlugin().androidInfo;
    if (androidInfo.version.sdkInt >= 31) {
      final statuses = await [
        Permission.bluetoothScan,
        Permission.bluetoothConnect,
      ].request();
      return statuses.values.every((status) => status.isGranted);
    } else {
      final statuses = await [
        Permission.bluetooth,
        Permission.location,
      ].request();
      return statuses.values.every((status) => status.isGranted);
    }
  }

  Future<void> _handleConnect() async {
    final hasPermission = await _requestPermissions();
    if (!hasPermission) {
      if (mounted) {
        _showAlert('Error', 'Bluetooth permissions are required');
      }
      return;
    }

    try {
      setState(() {
        _status = 'Scanning for devices...';
      });

      final foundDevices = await NiimbotBluetoothClient.listDevices(
        timeout: const Duration(seconds: 3),
      );
      final connectedDevices = FlutterBluePlus.connectedDevices;

      if (foundDevices.isEmpty && connectedDevices.isEmpty) {
        if (mounted) {
          _showAlert(
            'No Devices Found',
            'No compatible printers found. Make sure your printer is turned on and in pairing mode.',
          );
          setState(() => _status = 'No devices found');
        }
        return;
      }

      // Combine and deduplicate devices
      final allDevices = [...foundDevices, ...connectedDevices];
      final uniqueDevices = <BluetoothDevice>[];
      final seenIds = <String>{};
      for (final device in allDevices) {
        final id = device.remoteId.str;
        if (!seenIds.contains(id)) {
          seenIds.add(id);
          uniqueDevices.add(device);
        }
      }

      setState(() {
        _devices = uniqueDevices;
        _showDeviceList = true;
        _status = 'Select a device';
      });
    } catch (error) {
      if (mounted) {
        if (error.toString().contains('Bluetooth is not powered on')) {
          _showAlert(
            'Bluetooth Required',
            'Please enable Bluetooth in your device settings and try connecting again.',
          );
        } else {
          _showAlert('Error', error.toString());
        }
        setState(() => _status = 'Scan failed');
      }
    }
  }

  Future<void> _connectToDevice(BluetoothDevice device) async {
    setState(() {
      _showDeviceList = false;
      _status = 'Connecting...';
    });

    try {
      final client = NiimbotBluetoothClient();
      client.setDevice(device);
      final result = await client.connect();
      setState(() {
        _client = client;
        _status = 'Connected to ${result.deviceName}';
      });
      _client!.startHeartbeat();
    } catch (error) {
      if (mounted) {
        if (error.toString().contains('Bluetooth is not powered on')) {
          _showAlert(
            'Bluetooth Required',
            'Please enable Bluetooth in your device settings and try connecting again.',
          );
        } else {
          _showAlert('Error', error.toString());
        }
        setState(() => _status = 'Connection failed');
      }
    }
  }

  Future<void> _handleDisconnect() async {
    if (_client == null) return;

    try {
      await _client!.abstraction.printEnd();
      await _client!.disconnect();
      setState(() {
        _status = 'Disconnected';
        _client = null;
      });
    } catch (error) {
      _showAlert('Error', 'Failed to disconnect: ${error.toString()}');
    }
  }

  Future<void> _showPreviewAndPrint(
    PrintPage page,
    Future<void> Function() printAction,
  ) async {
    try {
      final imageData = await page.toPreviewImage();
      setState(() {
        _previewImage = imageData;
        _pendingPrintAction = printAction;
        _showPreview = true;
      });
    } catch (error) {
      _showAlert('Error', 'Failed to generate preview: ${error.toString()}');
    }
  }

  Future<void> _executePrint() async {
    setState(() => _showPreview = false);

    if (_client == null || !_client!.isConnected()) {
      _showAlert('Error', 'Not connected');
      setState(() => _pendingPrintAction = null);
      return;
    }

    if (_pendingPrintAction != null) {
      await _pendingPrintAction!();
      setState(() => _pendingPrintAction = null);
    }
  }

  Future<void> _executePrintTask(PrintPage page, String successMessage) async {
    if (_client == null || !_client!.isConnected()) {
      throw Exception('Not connected');
    }

    try {
      _client!.stopHeartbeat();
      _client!.packetIntervalMs = 0;

      final task = _client!.createPrintTask(
        const PrintOptions(
          totalPages: 1,
          density: 3,
          labelType: LabelType.withGaps,
          statusPollIntervalMs: 100,
          statusTimeoutMs: 8000,
        ),
      );

      if (task == null) {
        throw Exception(
          'Failed to create print task - printer model not detected',
        );
      }

      await task.printInit();
      await task.printPage(page.toEncodedImage(), 1);
      await task.waitForFinished();

      _client!.startHeartbeat();
      if (mounted) {
        _showAlert('Success', successMessage);
      }
    } catch (error) {
      _client!.startHeartbeat();
      if (mounted) {
        _showAlert('Error', 'Failed to print: ${error.toString()}');
      }
    }
  }

  Future<void> _handlePrint() async {
    if (_client == null || !_client!.isConnected()) {
      _showAlert('Error', 'Not connected');
      return;
    }

    final page = PrintPage(8, 1);
    for (int i = 0; i < 8; i++) {
      page.addLine(
          const LineOptions(x: 0, y: 0, endX: 0, endY: 0, thickness: 1));
    }

    await _executePrintTask(page, 'Print sent');
  }

  Future<void> _handlePrintSimple() async {
    // Simple demo without text rendering - just QR and barcode
    final page = PrintPage(400, 240);

    page.addQR(
      'Hello Niimbot',
      const QROptions(
        x: 100,
        y: 120,
        width: 100,
        height: 100,
        align: HAlignment.center,
        vAlign: VAlignment.middle,
      ),
    );

    page.addBarcode(
      '123456789012',
      const BarcodeOptions(
        encoding: BarcodeEncoding.ean13,
        x: 300,
        y: 120,
        width: 150,
        height: 60,
        align: HAlignment.center,
        vAlign: VAlignment.middle,
      ),
    );

    await _showPreviewAndPrint(page, () async {
      await _executePrintTask(page, 'Simple demo printed');
    });
  }

  Future<void> _handlePrintBoldText() async {
    final page = PrintPage(400, 240);

    await page.addText(
        'Normal Text',
        const TextOptions(
          x: 200,
          y: 60,
          fontSize: 18,
          align: HAlignment.center,
          vAlign: VAlignment.middle,
        ));

    await page.addText(
        'Bold Text',
        const TextOptions(
          x: 200,
          y: 120,
          fontSize: 18,
          fontWeight: FontWeight.bold,
          align: HAlignment.center,
          vAlign: VAlignment.middle,
        ));

    await page.addText(
        'Light Text',
        const TextOptions(
          x: 200,
          y: 180,
          fontSize: 18,
          fontWeight: FontWeight.w300,
          align: HAlignment.center,
          vAlign: VAlignment.middle,
        ));

    await _showPreviewAndPrint(page, () async {
      await _executePrintTask(page, 'Styled text printed');
    });
  }

  Future<void> _handlePrintLandscape() async {
    final page = PrintPage(320, 480, PageOrientation.landscape);

    page.addQR(
      'Landscape',
      const QROptions(
        x: 240,
        y: 240,
        width: 80,
        height: 80,
        align: HAlignment.center,
        vAlign: VAlignment.middle,
      ),
    );

    page.addBarcode(
      '987654321098',
      const BarcodeOptions(
        encoding: BarcodeEncoding.code128,
        x: 240,
        y: 80,
        width: 200,
        height: 60,
        align: HAlignment.center,
        vAlign: VAlignment.middle,
      ),
    );

    page.addLine(
        const LineOptions(x: 40, y: 300, endX: 440, endY: 300, thickness: 2));

    await page.addText(
        'LANDSCAPE MODE',
        const TextOptions(
          x: 240,
          y: 160,
          fontSize: 24,
          align: HAlignment.center,
          vAlign: VAlignment.middle,
        ));

    final heartData = [
      0,
      0,
      0,
      0,
      0,
      0,
      0,
      0,
      0,
      0,
      0,
      0,
      0,
      0,
      0,
      0,
      0,
      0,
      0,
      0,
      0,
      0,
      0,
      0,
      0,
      0,
      0,
      0,
      0,
      0,
      0,
      0,
      0,
      0,
      0,
      0,
      1,
      1,
      0,
      0,
      0,
      0,
      1,
      1,
      0,
      0,
      0,
      0,
      0,
      0,
      0,
      1,
      1,
      1,
      1,
      0,
      0,
      1,
      1,
      1,
      1,
      0,
      0,
      0,
      0,
      0,
      1,
      1,
      1,
      1,
      1,
      1,
      1,
      1,
      1,
      1,
      1,
      1,
      0,
      0,
      0,
      0,
      1,
      1,
      1,
      1,
      1,
      1,
      1,
      1,
      1,
      1,
      1,
      1,
      0,
      0,
      0,
      0,
      1,
      1,
      1,
      1,
      1,
      1,
      1,
      1,
      1,
      1,
      1,
      1,
      0,
      0,
      0,
      0,
      0,
      1,
      1,
      1,
      1,
      1,
      1,
      1,
      1,
      1,
      1,
      0,
      0,
      0,
      0,
      0,
      0,
      0,
      1,
      1,
      1,
      1,
      1,
      1,
      1,
      1,
      0,
      0,
      0,
      0,
      0,
      0,
      0,
      0,
      0,
      1,
      1,
      1,
      1,
      1,
      1,
      0,
      0,
      0,
      0,
      0,
      0,
      0,
      0,
      0,
      0,
      0,
      1,
      1,
      1,
      1,
      0,
      0,
      0,
      0,
      0,
      0,
      0,
      0,
      0,
      0,
      0,
      0,
      0,
      1,
      1,
      0,
      0,
      0,
      0,
      0,
      0,
      0,
      0,
      0,
      0,
      0,
      0,
      0,
      0,
      0,
      0,
      0,
      0,
      0,
      0,
      0,
      0,
      0,
      0,
      0,
      0,
      0,
      0,
      0,
      0,
      0,
      0,
      0,
      0,
      0,
      0,
      0,
      0,
      0,
      0,
      0,
      0,
      0,
      0,
      0,
      0,
      0,
      0,
      0,
      0,
      0,
      0,
      0,
      0,
      0,
      0,
      0,
      0,
      0,
      0,
      0,
      0,
      0,
      0,
      0,
      0,
      0,
      0,
      0,
      0,
      0,
    ];

    page.addPixelData(
      ImageOptions(
        data: heartData,
        imageWidth: 16,
        imageHeight: 11,
        x: 480,
        y: 0,
        width: 80,
        height: 80,
        align: HAlignment.right,
        vAlign: VAlignment.top,
      ),
    );

    // Add image from URL (async)
    try {
      await page.addImageFromUri(
        'https://fastly.picsum.photos/id/1/100/100.jpg?hmac=ZFE9J9JWYx84uJzvjw4GTuagMzN4FAmaKE4XeJDMZTY',
        const ImageFromBufferOptions(
          x: 0,
          y: 0,
          width: 100,
          height: 70,
          align: HAlignment.left,
          vAlign: VAlignment.top,
          threshold: 128,
        ),
      );
    } catch (e) {
      print('Failed to load image from URL: $e');
    }

    await _showPreviewAndPrint(page, () async {
      await _executePrintTask(page, 'Landscape page printed');
    });
  }

  Future<void> _handlePrintComprehensive() async {
    final page = PrintPage(400, 240);

    // QR Code top left with rotation
    page.addQR(
      'Hello QR',
      const QROptions(
        x: 60,
        y: 60,
        width: 80,
        height: 80,
        align: HAlignment.center,
        vAlign: VAlignment.middle,
        rotate: 15,
      ),
    );

    await page.addText(
        'Hello NIIMBOT!',
        const TextOptions(
          x: 200,
          y: 120,
          fontSize: 32,
          fontWeight: FontWeight.bold,
          align: HAlignment.center,
          vAlign: VAlignment.middle,
        ));

    await page.addText(
      'v1.0',
      const TextOptions(
        x: 350,
        y: 30,
        fontSize: 20,
        fontWeight: FontWeight.bold,
        align: HAlignment.center,
        vAlign: VAlignment.middle,
        rotate: 45,
      ),
    );

    // Heart image center
    final heartData = [
      0,
      0,
      0,
      0,
      0,
      0,
      0,
      0,
      0,
      0,
      0,
      0,
      0,
      0,
      0,
      0,
      0,
      0,
      0,
      0,
      0,
      0,
      0,
      0,
      0,
      0,
      0,
      0,
      0,
      0,
      0,
      0,
      0,
      0,
      0,
      0,
      1,
      1,
      0,
      0,
      0,
      0,
      1,
      1,
      0,
      0,
      0,
      0,
      0,
      0,
      0,
      1,
      1,
      1,
      1,
      0,
      0,
      1,
      1,
      1,
      1,
      0,
      0,
      0,
      0,
      0,
      1,
      1,
      1,
      1,
      1,
      1,
      1,
      1,
      1,
      1,
      1,
      1,
      0,
      0,
      0,
      0,
      1,
      1,
      1,
      1,
      1,
      1,
      1,
      1,
      1,
      1,
      1,
      1,
      0,
      0,
      0,
      0,
      1,
      1,
      1,
      1,
      1,
      1,
      1,
      1,
      1,
      1,
      1,
      1,
      0,
      0,
      0,
      0,
      0,
      1,
      1,
      1,
      1,
      1,
      1,
      1,
      1,
      1,
      1,
      0,
      0,
      0,
      0,
      0,
      0,
      0,
      1,
      1,
      1,
      1,
      1,
      1,
      1,
      1,
      0,
      0,
      0,
      0,
      0,
      0,
      0,
      0,
      0,
      1,
      1,
      1,
      1,
      1,
      1,
      0,
      0,
      0,
      0,
      0,
      0,
      0,
      0,
      0,
      0,
      0,
      1,
      1,
      1,
      1,
      0,
      0,
      0,
      0,
      0,
      0,
      0,
      0,
      0,
      0,
      0,
      0,
      0,
      1,
      1,
      0,
      0,
      0,
      0,
      0,
      0,
      0,
      0,
      0,
      0,
      0,
      0,
      0,
      0,
      0,
      0,
      0,
      0,
      0,
      0,
      0,
      0,
      0,
      0,
      0,
      0,
      0,
      0,
      0,
      0,
      0,
      0,
      0,
      0,
      0,
      0,
      0,
      0,
      0,
      0,
      0,
      0,
      0,
      0,
      0,
      0,
      0,
      0,
      0,
      0,
      0,
      0,
      0,
      0,
      0,
      0,
      0,
      0,
      0,
      0,
      0,
      0,
      0,
      0,
      0,
      0,
      0,
      0,
      0,
      0,
      0,
    ];

    page.addPixelData(
      ImageOptions(
        data: heartData,
        imageWidth: 16,
        imageHeight: 11,
        x: 200,
        y: 80,
        width: 50,
        height: 35,
        align: HAlignment.center,
        vAlign: VAlignment.middle,
        rotate: 30,
      ),
    );

    // Barcode bottom
    page.addBarcode(
      '123456789012',
      const BarcodeOptions(
        encoding: BarcodeEncoding.ean13,
        x: 200,
        y: 180,
        width: 150,
        height: 40,
        align: HAlignment.center,
        vAlign: VAlignment.middle,
      ),
    );

    page.addLine(
        const LineOptions(x: 50, y: 220, endX: 350, endY: 220, thickness: 1));

    await _showPreviewAndPrint(page, () async {
      await _executePrintTask(page, 'Comprehensive demo printed');
    });
  }

  void _showAlert(String title, String message) {
    showDialog(
      context: context,
      builder: (context) => AlertDialog(
        title: Text(title),
        content: Text(message),
        actions: [
          TextButton(
            onPressed: () => Navigator.pop(context),
            child: const Text('OK'),
          ),
        ],
      ),
    );
  }

  @override
  Widget build(BuildContext context) {
    return Stack(
      children: [
        Scaffold(
          backgroundColor: const Color(0xFFF5FCFF),
          body: SafeArea(
            child: SingleChildScrollView(
              child: Center(
                child: Column(
                  mainAxisAlignment: MainAxisAlignment.center,
                  children: [
                    const SizedBox(height: 40),
                    const Text(
                      'NiimBlueLibRN',
                      style: TextStyle(
                        fontSize: 24,
                        fontWeight: FontWeight.bold,
                        color: Colors.red,
                      ),
                    ),
                    const SizedBox(height: 20),
                    Text(
                      'Status: $_status',
                      style: const TextStyle(fontSize: 16),
                    ),
                    const SizedBox(height: 20),
                    _buildButton('πŸ”Œ Connect to Printer', _handleConnect,
                        Colors.blue[700]!),
                    _buildButton(
                        '⏏️ Disconnect', _handleDisconnect, Colors.blue[700]!),
                    _buildButton('πŸ–¨οΈ Quick Print Test', _handlePrint,
                        Colors.blue[700]!),
                    _buildButton('πŸ…°οΈ Bold Text Demo', _handlePrintBoldText,
                        Colors.blue[700]!),
                    _buildButton('πŸ“‹ All-in-One Demo',
                        _handlePrintComprehensive, const Color(0xFF5856D6)),
                    _buildButton('🎨 Simple Demo', _handlePrintSimple,
                        const Color(0xFF34C759)),
                    _buildButton('πŸ“„ Landscape Mode', _handlePrintLandscape,
                        const Color(0xFF34C759)),
                    const SizedBox(height: 40),
                  ],
                ),
              ),
            ),
          ),
        ),
        if (_showDeviceList) _buildDeviceSelectionModal(),
        if (_showPreview) _buildPreviewModal(),
      ],
    );
  }

  Widget _buildButton(String label, VoidCallback onPressed, Color color) {
    return Container(
      width: MediaQuery.of(context).size.width * 0.8,
      margin: const EdgeInsets.symmetric(vertical: 10),
      child: ElevatedButton(
        onPressed: onPressed,
        style: ElevatedButton.styleFrom(
          backgroundColor: color,
          padding: const EdgeInsets.all(15),
          shape: RoundedRectangleBorder(
            borderRadius: BorderRadius.circular(5),
          ),
        ),
        child: Text(
          label,
          style: const TextStyle(
            color: Colors.white,
            fontSize: 16,
          ),
        ),
      ),
    );
  }

  Widget _buildDeviceSelectionModal() {
    return Container(
      color: Colors.black.withValues(alpha: 0.5),
      child: Center(
        child: Container(
          margin: const EdgeInsets.all(20),
          decoration: BoxDecoration(
            color: Colors.white,
            borderRadius: BorderRadius.circular(10),
          ),
          child: Column(
            mainAxisSize: MainAxisSize.min,
            children: [
              Padding(
                padding: const EdgeInsets.all(20),
                child: Text(
                  'Select a Device',
                  style: TextStyle(
                    fontSize: 20,
                    fontWeight: FontWeight.bold,
                    color: Colors.blue[700],
                  ),
                ),
              ),
              const Divider(height: 1),
              ConstrainedBox(
                constraints: BoxConstraints(
                  maxHeight: MediaQuery.of(context).size.height * 0.5,
                ),
                child: Material(
                  color: Colors.transparent,
                  child: ListView.builder(
                    shrinkWrap: true,
                    itemCount: _devices.length,
                    itemBuilder: (context, index) {
                      final device = _devices[index];
                      final name = device.platformName;
                      final id = device.remoteId.str;
                      return ListTile(
                        title: Text(name),
                        subtitle: Text(id),
                        onTap: () => _connectToDevice(device),
                      );
                    },
                  ),
                ),
              ),
              const Divider(height: 1),
              TextButton(
                onPressed: () => setState(() => _showDeviceList = false),
                child: const Text(
                  'Cancel',
                  style: TextStyle(color: Colors.red, fontSize: 16),
                ),
              ),
            ],
          ),
        ),
      ),
    );
  }

  Widget _buildPreviewModal() {
    return Material(
      color: Colors.transparent,
      child: Container(
        color: Colors.black.withValues(alpha: 0.5),
        child: Center(
          child: Container(
            margin: const EdgeInsets.all(20),
            decoration: BoxDecoration(
              color: Colors.white,
              borderRadius: BorderRadius.circular(10),
            ),
            child: Column(
              mainAxisSize: MainAxisSize.min,
              children: [
                Padding(
                  padding: const EdgeInsets.all(20),
                  child: Text(
                    'Preview',
                    style: TextStyle(
                      fontSize: 20,
                      fontWeight: FontWeight.bold,
                      color: Colors.blue[700],
                    ),
                  ),
                ),
                const Divider(height: 1),
                if (_previewImage != null)
                  Container(
                    color: Colors.black,
                    padding: const EdgeInsets.all(20),
                    child: Image.memory(
                      _previewImage!,
                      fit: BoxFit.contain,
                      height: MediaQuery.of(context).size.height * 0.4,
                    ),
                  ),
                const Divider(height: 1),
                Row(
                  mainAxisAlignment: MainAxisAlignment.spaceEvenly,
                  children: [
                    TextButton(
                      onPressed: () => setState(() => _showPreview = false),
                      child: const Text(
                        'Cancel',
                        style: TextStyle(color: Colors.red, fontSize: 16),
                      ),
                    ),
                    ElevatedButton(
                      onPressed: _executePrint,
                      style: ElevatedButton.styleFrom(
                        backgroundColor: const Color(0xFF34C759),
                        padding: const EdgeInsets.symmetric(
                          horizontal: 30,
                          vertical: 12,
                        ),
                      ),
                      child: const Text(
                        'Print',
                        style: TextStyle(color: Colors.white, fontSize: 16),
                      ),
                    ),
                  ],
                ),
                const SizedBox(height: 20),
              ],
            ),
          ),
        ),
      ),
    );
  }

  @override
  void dispose() {
    _client?.disconnect();
    super.dispose();
  }
}
1
likes
150
points
174
downloads

Publisher

unverified uploader

Weekly Downloads

Flutter library for BLE printing with NIIMBOT thermal printers. Supports 85+ printer models with automatic detection and rich content rendering.

Repository (GitHub)
View/report issues

Topics

#ble #printing #niimbot #thermal-printer #label-printer

Documentation

API reference

License

MIT (license)

Dependencies

barcode, crypto, flutter, flutter_blue_plus, http, image, qr, synchronized

More

Packages that depend on niim_blue_flutter