Today we are going to learn how to print via the thermal Bluetooth printer in Flutter. To accomplish this, we have to connect to a printer via Bluetooth and send printable data like text, image, or QR code. We will use these plugins: flutter_blue: ^0.8.0 esc_pos_utils: ^1.1.0 flutter_blue We'll use this plugin to connect to the Bluetooth device and to send the data. It is not only for thermal printers but also can be used for any Bluetooth device. I chose this plugin because it supports iOS, Android, and macOS. https://pub.dev/packages/flutter_blue esc_pos_utils This plugin will help to generate ESC/POS commands to send it via flutter_blue. It supports images, barcodes, text styling, tables, paper cut, paper feed, and much more. https://pub.dev/packages/esc_pos_utils Setup We may need permission setups, so go to the flutter_blue page and make setup according to instructions. Scan for devices We can scan and get available devices like this: FlutterBlue flutterBlue = FlutterBlue.instance; // Start scanning flutterBlue.startScan(timeout: Duration(seconds: 4)); // Listen to scan results var subscription = flutterBlue.scanResults.listen((results) { // do something with scan results for (ScanResult r in results) { print('${r.device.name} found! rssi: ${r.rssi}'); } }); // Stop scanning flutterBlue.stopScan(); But let's create a widget where We can show the list of devices. And when a device is selected generate, ESC/POS commands and send them to the selected device. import 'package:blue_thermal_printing/blue_print.dart'; import 'package:esc_pos_utils/esc_pos_utils.dart'; import 'package:flutter/material.dart'; import 'package:flutter_blue/flutter_blue.dart'; FlutterBlue flutterBlue = FlutterBlue.instance; class PrintingWidget extends StatefulWidget { const PrintingWidget({Key? key}) : super(key: key); @override _PrintingWidgetState createState() => _PrintingWidgetState(); } class _PrintingWidgetState extends State<PrintingWidget> { List<ScanResult>? scanResult; @override void initState() { super.initState(); findDevices(); } void findDevices() { flutterBlue.startScan(timeout: const Duration(seconds: 4)); flutterBlue.scanResults.listen((results) { setState(() { scanResult = results; }); }); flutterBlue.stopScan(); } void printWithDevice(BluetoothDevice device) async { await device.connect(); final gen = Generator(PaperSize.mm58, await CapabilityProfile.load()); final printer = BluePrint(); printer.add(gen.qrcode('https://altospos.com')); printer.add(gen.text('Hello')); printer.add(gen.text('World', styles: const PosStyles(bold: true))); printer.add(gen.feed(1)); await printer.printData(device); device.disconnect(); } @override Widget build(BuildContext context) { return Scaffold( appBar: AppBar(title: const Text('Bluetooth devices')), body: ListView.separated( itemBuilder: (context, index) { return ListTile( title: Text(scanResult![index].device.name), subtitle: Text(scanResult![index].device.id.id), onTap: () => printWithDevice(scanResult![index].device), ); }, separatorBuilder: (context, index) => const Divider(), itemCount: scanResult?.length ?? 0, ), ); } } Let's look at the printWithDevice method. First, we connect to the given device. The Generator is a class of the esc_pos_utils plugin. With the help of this class, we can create ESC/POS commands like this: generator.text('Hello'). We can add these generated commands to the BluePrint instance (we will create this class further). At the end, we send all generated data to the Bluetooth printer and disconnect when finished. Printing Now let's see the BluePrint class. In short, it collects ESC/POS commands and sends these data to a given printer. void add(List<int> data) - To add commands. List<List<int>> getChunks() - If we need to print very large data like an image, we may encounter problems because some devices will fail because of the big package size. So we need to send the data in chunks. printData(BluetoothDevice device) - Sends chunked data to the device via BluetoothCharacteristic. import 'dart:math'; import 'package:flutter_blue/flutter_blue.dart'; class BluePrint { BluePrint({this.chunkLen = 512}); final int chunkLen; final _data = List<int>.empty(growable: true); void add(List<int> data) { _data.addAll(data); } List<List<int>> getChunks() { final chunks = List<List<int>>.empty(growable: true); for (var i = 0; i < _data.length; i += chunkLen) { chunks.add(_data.sublist(i, min(i + chunkLen, _data.length))); } return chunks; } Future<void> printData(BluetoothDevice device) async { final data = getChunks(); final characs = await _getCharacteristics(device); for (var i = 0; i < characs.length; i++) { if (await _tryPrint(characs[i], data)) { break; } } } Future<bool> _tryPrint( BluetoothCharacteristic charac, List<List<int>> data, ) async { for (var i = 0; i < data.length; i++) { try { await charac.write(data[i]); } catch (e) { return false; } } return true; } Future<List<BluetoothCharacteristic>> _getCharacteristics( BluetoothDevice device, ) async { final services = await device.discoverServices(); final res = List<BluetoothCharacteristic>.empty(growable: true); for (var i = 0; i < services.length; i++) { res.addAll(services[i].characteristics); } return res; } } I needed to add a Bluetooth thermal printer feature when I was working on Alto's POS project. It was my first Bluetooth experience with Flutter, and because of the little material about it, I had to try many things before making it work properly on every device. So I decided to write this article in hope that it will be useful for others. The GitHub project is available here: https://github.com/usenbekov/blue-thermal-printing Today we are going to learn how to print via the thermal Bluetooth printer in Flutter. To accomplish this, we have to connect to a printer via Bluetooth and send printable data like text, image, or QR code. We will use these plugins: flutter_blue: ^0.8.0 flutter_blue: ^0.8.0 esc_pos_utils: ^1.1.0 esc_pos_utils: ^1.1.0 flutter_blue We'll use this plugin to connect to the Bluetooth device and to send the data. It is not only for thermal printers but also can be used for any Bluetooth device. I chose this plugin because it supports iOS, Android, and macOS. https://pub.dev/packages/flutter_blue https://pub.dev/packages/flutter_blue esc_pos_utils This plugin will help to generate ESC/POS commands to send it via flutter_blue. It supports images, barcodes, text styling, tables, paper cut, paper feed, and much more. https://pub.dev/packages/esc_pos_utils https://pub.dev/packages/esc_pos_utils Setup We may need permission setups, so go to the flutter_blue page and make setup according to instructions. flutter_blue Scan for devices We can scan and get available devices like this: FlutterBlue flutterBlue = FlutterBlue.instance; // Start scanning flutterBlue.startScan(timeout: Duration(seconds: 4)); // Listen to scan results var subscription = flutterBlue.scanResults.listen((results) { // do something with scan results for (ScanResult r in results) { print('${r.device.name} found! rssi: ${r.rssi}'); } }); // Stop scanning flutterBlue.stopScan(); FlutterBlue flutterBlue = FlutterBlue.instance; // Start scanning flutterBlue.startScan(timeout: Duration(seconds: 4)); // Listen to scan results var subscription = flutterBlue.scanResults.listen((results) { // do something with scan results for (ScanResult r in results) { print('${r.device.name} found! rssi: ${r.rssi}'); } }); // Stop scanning flutterBlue.stopScan(); But let's create a widget where We can show the list of devices. And when a device is selected generate, ESC/POS commands and send them to the selected device. We can show the list of devices. And when a device is selected generate, ESC/POS commands and send them to the selected device. import 'package:blue_thermal_printing/blue_print.dart'; import 'package:esc_pos_utils/esc_pos_utils.dart'; import 'package:flutter/material.dart'; import 'package:flutter_blue/flutter_blue.dart'; FlutterBlue flutterBlue = FlutterBlue.instance; class PrintingWidget extends StatefulWidget { const PrintingWidget({Key? key}) : super(key: key); @override _PrintingWidgetState createState() => _PrintingWidgetState(); } class _PrintingWidgetState extends State<PrintingWidget> { List<ScanResult>? scanResult; @override void initState() { super.initState(); findDevices(); } void findDevices() { flutterBlue.startScan(timeout: const Duration(seconds: 4)); flutterBlue.scanResults.listen((results) { setState(() { scanResult = results; }); }); flutterBlue.stopScan(); } void printWithDevice(BluetoothDevice device) async { await device.connect(); final gen = Generator(PaperSize.mm58, await CapabilityProfile.load()); final printer = BluePrint(); printer.add(gen.qrcode('https://altospos.com')); printer.add(gen.text('Hello')); printer.add(gen.text('World', styles: const PosStyles(bold: true))); printer.add(gen.feed(1)); await printer.printData(device); device.disconnect(); } @override Widget build(BuildContext context) { return Scaffold( appBar: AppBar(title: const Text('Bluetooth devices')), body: ListView.separated( itemBuilder: (context, index) { return ListTile( title: Text(scanResult![index].device.name), subtitle: Text(scanResult![index].device.id.id), onTap: () => printWithDevice(scanResult![index].device), ); }, separatorBuilder: (context, index) => const Divider(), itemCount: scanResult?.length ?? 0, ), ); } } import 'package:blue_thermal_printing/blue_print.dart'; import 'package:esc_pos_utils/esc_pos_utils.dart'; import 'package:flutter/material.dart'; import 'package:flutter_blue/flutter_blue.dart'; FlutterBlue flutterBlue = FlutterBlue.instance; class PrintingWidget extends StatefulWidget { const PrintingWidget({Key? key}) : super(key: key); @override _PrintingWidgetState createState() => _PrintingWidgetState(); } class _PrintingWidgetState extends State<PrintingWidget> { List<ScanResult>? scanResult; @override void initState() { super.initState(); findDevices(); } void findDevices() { flutterBlue.startScan(timeout: const Duration(seconds: 4)); flutterBlue.scanResults.listen((results) { setState(() { scanResult = results; }); }); flutterBlue.stopScan(); } void printWithDevice(BluetoothDevice device) async { await device.connect(); final gen = Generator(PaperSize.mm58, await CapabilityProfile.load()); final printer = BluePrint(); printer.add(gen.qrcode('https://altospos.com')); printer.add(gen.text('Hello')); printer.add(gen.text('World', styles: const PosStyles(bold: true))); printer.add(gen.feed(1)); await printer.printData(device); device.disconnect(); } @override Widget build(BuildContext context) { return Scaffold( appBar: AppBar(title: const Text('Bluetooth devices')), body: ListView.separated( itemBuilder: (context, index) { return ListTile( title: Text(scanResult![index].device.name), subtitle: Text(scanResult![index].device.id.id), onTap: () => printWithDevice(scanResult![index].device), ); }, separatorBuilder: (context, index) => const Divider(), itemCount: scanResult?.length ?? 0, ), ); } } Let's look at the printWithDevice method. printWithDevice First, we connect to the given device. The Generator is a class of the esc_pos_utils plugin. With the help of this class, we can create ESC/POS commands like this: generator.text('Hello'). We can add these generated commands to the BluePrint instance (we will create this class further). At the end, we send all generated data to the Bluetooth printer and disconnect when finished. First, we connect to the given device. The Generator is a class of the esc_pos_utils plugin. With the help of this class, we can create ESC/POS commands like this: generator.text('Hello') . Generator esc_pos_utils generator.text('Hello') We can add these generated commands to the BluePrint instance (we will create this class further). BluePrint At the end, we send all generated data to the Bluetooth printer and disconnect when finished. Printing Now let's see the BluePrint class. In short, it collects ESC/POS commands and sends these data to a given printer. void add(List<int> data) - To add commands. List<List<int>> getChunks() - If we need to print very large data like an image, we may encounter problems because some devices will fail because of the big package size. So we need to send the data in chunks. printData(BluetoothDevice device) - Sends chunked data to the device via BluetoothCharacteristic. void add(List<int> data) - To add commands. void add(List<int> data) List<List<int>> getChunks() - If we need to print very large data like an image, we may encounter problems because some devices will fail because of the big package size. So we need to send the data in chunks. List<List<int>> getChunks() printData(BluetoothDevice device) - Sends chunked data to the device via BluetoothCharacteristic. printData(BluetoothDevice device) import 'dart:math'; import 'package:flutter_blue/flutter_blue.dart'; class BluePrint { BluePrint({this.chunkLen = 512}); final int chunkLen; final _data = List<int>.empty(growable: true); void add(List<int> data) { _data.addAll(data); } List<List<int>> getChunks() { final chunks = List<List<int>>.empty(growable: true); for (var i = 0; i < _data.length; i += chunkLen) { chunks.add(_data.sublist(i, min(i + chunkLen, _data.length))); } return chunks; } Future<void> printData(BluetoothDevice device) async { final data = getChunks(); final characs = await _getCharacteristics(device); for (var i = 0; i < characs.length; i++) { if (await _tryPrint(characs[i], data)) { break; } } } Future<bool> _tryPrint( BluetoothCharacteristic charac, List<List<int>> data, ) async { for (var i = 0; i < data.length; i++) { try { await charac.write(data[i]); } catch (e) { return false; } } return true; } Future<List<BluetoothCharacteristic>> _getCharacteristics( BluetoothDevice device, ) async { final services = await device.discoverServices(); final res = List<BluetoothCharacteristic>.empty(growable: true); for (var i = 0; i < services.length; i++) { res.addAll(services[i].characteristics); } return res; } } import 'dart:math'; import 'package:flutter_blue/flutter_blue.dart'; class BluePrint { BluePrint({this.chunkLen = 512}); final int chunkLen; final _data = List<int>.empty(growable: true); void add(List<int> data) { _data.addAll(data); } List<List<int>> getChunks() { final chunks = List<List<int>>.empty(growable: true); for (var i = 0; i < _data.length; i += chunkLen) { chunks.add(_data.sublist(i, min(i + chunkLen, _data.length))); } return chunks; } Future<void> printData(BluetoothDevice device) async { final data = getChunks(); final characs = await _getCharacteristics(device); for (var i = 0; i < characs.length; i++) { if (await _tryPrint(characs[i], data)) { break; } } } Future<bool> _tryPrint( BluetoothCharacteristic charac, List<List<int>> data, ) async { for (var i = 0; i < data.length; i++) { try { await charac.write(data[i]); } catch (e) { return false; } } return true; } Future<List<BluetoothCharacteristic>> _getCharacteristics( BluetoothDevice device, ) async { final services = await device.discoverServices(); final res = List<BluetoothCharacteristic>.empty(growable: true); for (var i = 0; i < services.length; i++) { res.addAll(services[i].characteristics); } return res; } } I needed to add a Bluetooth thermal printer feature when I was working on Alto's POS project. It was my first Bluetooth experience with Flutter, and because of the little material about it, I had to try many things before making it work properly on every device. So I decided to write this article in hope that it will be useful for others. Alto's POS The GitHub project is available here: https://github.com/usenbekov/blue-thermal-printing https://github.com/usenbekov/blue-thermal-printing