Printing in Flutter via thermal bleutooth printer
Tags: Flutter, Android, iOS, Bluetooth Printer

Requirements — hardware

All you need to make it work is the following

Flutter installed on a computer

a device with bluetooth like an iPhone or a tablet

a bluetooth thermal printer.

You can use any type of blootooth printer. They are pretty cheap and any printer will do the job: the one I have used recently was given to me by a restaurant and it is selling at £39 in the uk through amazon, see here the link.

Requirement — software

We defenitively want to use few plugins. As always kudos to those who wrote the code!!!

Here are the plugin you we will use:

esc_pos_bluetooth: ^0.2.8

flutter_bluetooth_basic: ^0.1.5

The first one allows to find bluetooth devices while the second one allows to print receipts using a Bluetooth printer. Please note we are using a bluetooth printer in this article; if you have a WiFi/Ethernet printers you can use the same logic but you need to use the esc_pos_printer library.

Set up

In order to use the bluetooth functionality you must do some nasty stuff. You basically need to access the device location.

…wait what?!?!

Yes you have heard right. You need to grant location permission to use bluetooth. It seems strange but applications using Bluetooth must have direct access to the device MAC address for the purpose of pairing. If you can read MAC addresses of WiFi or Bluetooth transmitters, you can locate a device. This is how WiFi/Bluetooth location works; you listen for MAC addresses of transmitters and look these up in your list.

For Android, first of all make sure you app minimum sdk is version 19 at least. Check the android/app/build.gradle and change it accordingly:

Android {

defaultConfig {

minSdkVersion: 19

Then add the permission to use Bluetooth and access location in the AndroidManifest.xml as follow:

<uses-permission android:name="android.permission.BLUETOOTH" />

<uses-permission android:name="android.permission.BLUETOOTH_ADMIN" />

<uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION"/>

In iOS you need to make some changes to the Info.plist. Let’s add:

<key>NSBluetoothAlwaysUsageDescription</key>

<string>Need BLE permission</string>

<key>NSBluetoothPeripheralUsageDescription</key>

<string>Need BLE permission</string>

<key>NSLocationAlwaysAndWhenInUseUsageDescription</key>

<string>Need Location permission</string>

<key>NSLocationAlwaysUsageDescription</key>

<string>Need Location permission</string>

<key>NSLocationWhenInUseUsageDescription</key>

<string>Need Location permission</string>

Overview of the process

The workflow is pretty simple:

Step1: Generate something to print (a String or a List of strings)

Step 2: Look for bluetooth device

Step 3: Print

We will create two dart files:

main.dart: this is where we will generate the strings to print

print.dart: this is where we will scan for bluetooth devices and print

Let’s begin!

Step 1: Generate something to print (main.dart)

Go ahead and create a basic new flutter app, the one that will count the number of times you have tapped on a floating button. If you don’t know what I am talking about you have never done a flutter app, so go back here and study a bit before trying to print via bluetooth.

Now, what we want to do is to tap on the floating button and transfer some text into a new screen. Let’s pretend we need to print an order we have just received from a customer. In this example I am passing just some static info using strings and a List. In your real app you want to pass dynamic data…

Add the following code to your onPressed method:

List<String> myList = [];

for (int i = 0; i < 4; ++i) {

myList.add('Item nr $i');

}

Navigator.push(

context,

CupertinoPageRoute(

builder: (context) => PrintOrders(

orderType: 'demo',

orderNumber: '123456',

customerName: 'John',

deliveryTime: 'asap',

instruction: 'Are you going to be able to print this one?',

items: myList,

),

));

We have not yet created the PrintOrders class so sooner or later remember to come back here to import that class in your main file. It is going to be something like this

import 'package:myamazingprinterapp/print.dart';

That’s all for your main dart

Step 2: Look for bluetooth device (print.dart)

Now let’s create a new dart file and call it print.dart

We are going to import the following packages in our dart file:

import 'package:flutter_bluetooth_basic/flutter_bluetooth_basic.dart';

import 'package:esc_pos_bluetooth/esc_pos_bluetooth.dart';

import 'package:esc_pos_utils/esc_pos_utils.dart';

First create the space to receive the info from your main file

class PrintOrders extends StatefulWidget {

final String orderType;

final String orderNumber;

final String customerName;

final String deliveryTime;

final String instruction;

final List<String> items;



PrintOrders(

{Key key, @required this.orderNumber, @required this.orderType, @required this.instruction, @required this.items, @required this.customerName, @required this.deliveryTime})

: super(key: key);



@override

_PrintOrdersState createState() => _PrintOrdersState();

}

Then define the following objects

PrinterBluetoothManager _printerManager = PrinterBluetoothManager();

List<PrinterBluetooth> _devices = [];

String _devicesMsg;

BluetoothManager bluetoothManager = BluetoothManager.instance;

The _devices list will contain all the bluetooth device discovered during the scan

You main widget will be as follow

@override

Widget build(BuildContext context) {

return Scaffold( appBar: AppBar(

title: Text("Printer page"),

),

backgroundColor: kColorPrimary,

body: MyProgressIndicator(

inAsyncCall: showSpinner,

child: _devices.isNotEmpty

? ListView.builder(

itemBuilder: (context, position) => ListTile(

onTap: () {

// _startPrint(_devices[position]);

},

leading: Icon(Icons.print),

title: Text(_devices[position].name),

subtitle: Text(_devices[position].address),

),

itemCount: _devices.length,

)

: Center(

child: Text(

_devicesMsg ?? 'Ops something went wrong!',

style: TextStyle(fontSize: 24),

),

),

)),

);

}

If the _devices object will be empty then we will show a simple ‘Ops’ message otherwise we will present the list of devices.

Try to run your code. If you tap on the floating button you should get to the Ops page. Ok lets continue

We now want to scan for devices and we will do it with an initPrinter method

void initPrinter() {

print('init printer');



_printerManager.startScan(Duration(seconds: 2));

_printerManager.scanResults.listen((event) {



if (!mounted) return;

setState(() => _devices = event);



if (_devices.isEmpty)

setState(() {

_devicesMsg = 'No devices';

});

});

}

We are basically giving us 2 seconds to scan the network and then we scan for results and save them in the _devices list.

It looks easy but we need to make sure we are able to scan the network. This is an issue with Android only. In Android we should not initiate the scan if the bluetooth is not enabled otherwise we will have an error. In order to do so, when the screen will load, we will check what is the value of bluetoothManager.state and initiate initPrinter() only when we are ready. You may need to test your own printer and see which value is ok. In my case bluetoothManager.state = 12 means good to go, while a value of 10 means that bluetooth is not enabled. Have a look at the init method below

@override

void initState() {

if (appData.isIOS) {

initPrinter();

} else {

bluetoothManager.state.listen((val) {

print("state = $val");

if (!mounted) return;

if (val == 12) {

print('on');

initPrinter();

} else if (val == 10) {

print('off');

setState(() {

_devicesMsg = 'Please enable bluetooth to print';

});

}

print('state is $val');

});

}

super.initState();

}

Step 3: Print (print.dart)

The print is relatively easy. We need to build a ticket object with the data we want to print and then simply run the _printerManager.printTicket. Make sure to set the relevant paper size in line with your printer, in my case it is a mm58.

Future<void> _startPrint(PrinterBluetooth printer) async {

_printerManager.selectPrinter(printer);

final myTicket = await _ticket(PaperSize.mm58);

final result = await _printerManager.printTicket(myTicket);

print(result);

}



Future<Ticket> _ticket(PaperSize paper) async {

final ticket = Ticket(paper);

ticket.text(widget.orderType);

ticket.text(widget.orderNumber);

ticket.text(widget.customerName);

ticket.text(widget.deliveryTime);

ticket.text(widget.instruction);



ticket.cut();

return ticket;

}

You are done. Run you project and print !