Thermal Print With Flutter

Onur Bulut
6 min readOct 1, 2023

--

Did you know that your code can actually affect physical devices?

This was my first touch with my code to physical objects and it thrilled me!

Here is the way to communicate with a device with bluetooth;

First thing first we need a plugin for device control. There are too many packages on “pub.dev”. My choices are “esc_pos_utils_plus” and “print_bluetooth_thermal” packages.

Esc pos is a standard of receipt world. We use “esc_pos_utils_plus” as a design tool and the other package “print_bluetooth_thermal” etc. for printer communication. If you want to use the most popular package you need to have “esc_pos_printer”.

Install Packages

flutter pub add print_bluetooth_thermal
flutter pub add esc_pos_utils

We want to use these packages but the operating systems should know us otherwise they do not accept communication to codes.

Hello OS, this is us!

For IOS

Go to the “Runner” folder under the “ios” folder then locate the “info.plist” and insert the following two lines of code.

ios > Runner > info.plist

<key>NSBluetoothAlwaysUsageDescription</key>
<string>Bluetooth access to connect 58mm or 80mm thermal printers</string>

For Android OS

Go to the “main” folder under the “src” folder under the “android” folder then “AndroidManifest.xml” and add the following codes between the “<manifest>” tags.

android > src > main > AndroidManifest.xml

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

Manifest tag full example;

  <manifest xmlns:android="http://schemas.android.com/apk/res/android">
<uses-permission android:name="android.permission.INTERNET"/>
<uses-permission android:name= "android.permission.BLUETOOTH_CONNECT"/>
<application
android:label="example"
android:name="${applicationName}"
android:icon="@mipmap/ic_launcher">
<activity
android:name=".MainActivity"
android:exported="true"
android:launchMode="singleTop"
android:theme="@style/LaunchTheme"
android:configChanges="orientation|keyboardHidden|keyboard|screenSize|smallestScreenSize|locale|layoutDirection|fontScale|screenLayout|density|uiMode"
android:hardwareAccelerated="true"
android:windowSoftInputMode="adjustResize">
<!-- Specifies an Android theme to apply to this Activity as soon as
the Android process has started. This theme is visible to the user
while the Flutter UI initializes. After that, this theme continues
to determine the Window background behind the Flutter UI. -->
<meta-data
android:name="io.flutter.embedding.android.NormalTheme"
android:resource="@style/NormalTheme"
/>
<intent-filter>
<action android:name="android.intent.action.MAIN"/>
<category android:name="android.intent.category.LAUNCHER"/>
</intent-filter>
</activity>
<!-- Don't delete the meta-data below.
This is used by the Flutter tool to generate GeneratedPluginRegistrant.java -->
<meta-data
android:name="flutterEmbedding"
android:value="2" />
</application>
</manifest>

Excellent!

We are going to forward now.

Add the following code to top your “main.dart” file;

import 'package:print_bluetooth_thermal/print_bluetooth_thermal.dart';

We can use the “PrintBluetoothThermal” class now!

We have a PrintBluetoothThermal class and the class has a static method named “pairedBluetooths”.

await PrintBluetoothThermal.pairedBluetooths;

We used the “await” keyword because the method type is an async/future function. Then the method returns the “BluetoothInfo” class in a list.

Method description
Method description

The “BluetoothInfo” class has two fields; “name” and “macAdress”.

BluetoothInfo class fields

We get the devices on a list!

If we choose a device, the next step is to connect to the device.

“connect” method provides communication between two devices.

await connect(macPrinterAddress: mac);

Then it returns to us “It is okay” or “There is a problem here!”. Which means “true” or “false”.

Connect method description

Congrats! We connected!

We Should Tell The Printer What We Want To Print

We should set the page settings like page width, like code table which means ASCII charts.
In movable thermal printers usually have two popular page roll widths. These are 57mm and 80mm.

Firstly, we need to “CapabilityProfile” class for the setting operation start. “CapabilityProfile” class from “esc_pos_utils_plus” package.

import 'package:esc_pos_utils_plus/esc_pos_utils.dart';

We are going to create an instance;

CapabilityProfile profile = await CapabilityProfile.load();

Next, we going to set the page size with “Generator” class and “CapabilityProfile” instance in this case it is “profile”.

Generator generator = Generator(PaperSize.mm80, profile);

The next step is to decide what we should print. We going to prepare with list of “byte”. Every property or character going to be byte and have to. So we need a list of byte instance. We going to add all property or characters to this list.

List<int> bytes = [];

Good news!
If we want, we can print images too.

For this case, we use “rootBundle” instance from in “asset_bundle.dart” file. The file is embedded in Flutter. So you can access it everywhere.

We need conversion operations;

First, we get the image as a “ByteData” object,

ByteData imageByteData =
await rootBundle.load("assets/images/${imagePath}");

And we have “ByteData” now.

Next step, we get “Uint8List” from“ByteData”.

Uint8List imageBytesUint8List = imageByteData.buffer.asUint8List();

Very very nice!
We got “Uint8List”. We are ready for the last conversion to “Image” class. The class from “image” package. So, you should add to your dependencies.

name: project_name
description: Printer example project.
version: 1.0.0+1
publish_to: none
environment:
sdk: '>=3.0.5 <4.0.0'

dependencies:
flutter:
sdk: flutter

image: ^3.2.0

Now, import the package to your file. If you use “material” package same file, there will be a conflict problem here. Here is the solving way;

The first way is hiding a class from the other one which means an unused package going to be hidden. The magic keyword is “hide” here.

import 'package:flutter/material.dart' hide Image;

The second way is adding prefixes to import sentences. This operation is like using an instance. The magic keyword is “as” here.

import 'package:image/image.dart' as image;

Usage example and last conversion with “decodeImage” method from image package;

// With as
image.Image image = image.decodeImage(imageBytesUint8List)!;

// With hide
Image image = decodeImage(imageBytesUint8List)!;

Now, the image going to add to the list.

bytes += generator.image(image);

Now, the step is adding text with style. For styling we are going to use “PosStyles” class from “esc_pos_utils_plus” package;

bytes += generator.text("My First Printing"),
styles: const PosStyles(bold: true, underline: true));

The other properties of “PosStyles”;

PosStyles class fields
PosStyles class fields

Tables!

We can create tables. If the first row can be a header row, that would be perfect!

bytes += generator.row([
PosColumn(text: "Header 1", width: 4, styles: PosStyles(bold: true, underline: false)),
PosColumn(text: "Header 2", width: 4, styles: PosStyles(bold: true, underline: false)),
PosColumn(text: "Header 3", width: 4, styles: PosStyles(bold: true, underline: false)),
]);

Now we are going to add data cells the same way;

bytes += generator.row([
PosColumn(text: "R1,Cell 1", width: 4),
PosColumn(text: "R1,Cell 2", width: 4),
PosColumn(text: "R1,Cell 3", width: 4),
]);
bytes += generator.row([
PosColumn(text: "R2,Cell 4", width: 4),
PosColumn(text: "R2,Cell 5", width: 4),
PosColumn(text: "R2,Cell 6", width: 4),
]);

If you want to divide row by row with hyphens, you should use “hr” method from “Generator” class.

bytes += generator.hr();

Table usage with hyphens;

bytes += generator.row([
PosColumn(text: "Header 1", width: 4, styles: PosStyles(bold: true, underline: false)),
PosColumn(text: "Header 2", width: 4, styles: PosStyles(bold: true, underline: false)),
PosColumn(text: "Header 3", width: 4, styles: PosStyles(bold: true, underline: false)),
]);

bytes += generator.hr();
bytes += generator.hr();

bytes += generator.row([
PosColumn(text: "R1,Cell 1", width: 4),
PosColumn(text: "R1,Cell 2", width: 4),
PosColumn(text: "R1,Cell 3", width: 4),
]);

bytes += generator.hr();

bytes += generator.row([
PosColumn(text: "R2,Cell 4", width: 4),
PosColumn(text: "R2,Cell 5", width: 4),
PosColumn(text: "R2,Cell 6", width: 4),
]);

bytes += generator.hr();

Ready to Print!

Finally, we are going to print now.

await PrintBluetoothThermal.writeBytes(bytes);

> Mission accomplished :)

This image for your imagine

Voila!

--

--