universal_thermal_printer 1.0.0
universal_thermal_printer: ^1.0.0 copied to clipboard
Universal Thermal Printer SDK for Flutter — Bluetooth & Network (WiFi/LAN) ESC/POS thermal printer support with text, QR, barcode, images, and built-in remote license activation.
universal_thermal_printer #
A production-grade Flutter SDK for thermal printers across any industry — retail, restaurants, logistics, pharmacy, banking, parking, warehousing and more. Bluetooth Classic + BLE + USB + Network, a composable template engine with industry-ready layouts, a built-in print preview, auto-reconnect, an offline queue, real-time status, and transparent licensing.
The SDK is designed to feel like a natural part of your app: connect a printer, pick a template, preview it, print. No license key is ever shown to your end users — licensing is an internal detail you configure once (or not at all).
Highlights #
- Every transport: Bluetooth Classic (SPP), Bluetooth LE, USB (Android host), Network (WiFi/LAN, port 9100), with auto-discovery so users never type an IP.
- Multi-printer routing: hold a
receiptand akitchenprinter at once and send each job to the right one (PrinterManager). - Resilience: automatic reconnect with backoff, an offline print queue that flushes on reconnect, real-time status (paper out / cover open / offline / low battery), and typed exceptions.
- Paper-aware templates: 58mm and 80mm profiles + label vs continuous stock; the template engine adapts column widths automatically.
- Composable industry elements:
TableElement,TaxBreakdownElement,AddressBlockElement,BarcodeLabelElement,BigNumberElement— plus 8 ready-to-usePrebuiltTemplates. - Live preview widget:
ReceiptPreviewrenders a template on-screen exactly as it will print. - Pluggable command language: ESC/POS today; ZPL/TSPL/CPCL can be added
later without changing your code (
CommandProtocol).
Install #
dependencies:
universal_thermal_printer: ^1.0.0
Licensing — handled for you #
You do not pass a license key on every call, and your users never see one.
Configure the SDK once in main():
void main() {
// Unlimited printing — use the license key you received (see "Get a license"):
LicenseManager.instance.configure(licenseKey: 'UTP-XXXX-XXXX-XXXX');
runApp(MyApp());
}
If you don't provide a key, the SDK runs in free mode: it still prints, but is capped at 10 bills per day (resets daily, tracked on-device). That's enough for evaluation and very small shops. Control operations (paper feed, cut, cash drawer) never count against the limit.
// Zero config also works — starts in free mode automatically:
LicenseManager.instance.configure();
Check state any time (e.g. to show a banner):
LicenseManager.instance.mode; // LicenseMode.free / .licensed
LicenseManager.instance.remainingToday; // free prints left today, -1 = unlimited
A verified license keeps working offline: after a successful check, a
transient network error is honored for a grace period (default 72h,
configure(graceDuration: ...)), so a flaky connection never blocks a checkout.
Get a license / support #
To remove the daily limit, request a license key. For keys, pricing or any support:
📧 Contact:
shilpa.devloper@gmail.com(SDK owner: set this once inLicenseManager.supportContactand here in the README.)
Quick start: build a receipt and print it #
The fastest way to print a real receipt is the data-first Receipt builder —
add your header, line items, totals and footer with actual values (no templates,
no {{placeholders}}), then print in one call. It automatically adapts to
58mm/80mm paper, renders non-Latin text as bitmaps, cuts the paper, and flows
through the offline queue, retries and licensing.
final printer = ThermalPrinter(profile: PrinterProfile.mm80());
await printer.connectBluetooth(macAddress); // or BLE / USB / Network
final receipt = Receipt(currency: '₹')
..title('RAVI TRADERS')
..center('123 MG Road, Bengaluru')
..center('GSTIN: 29ABCDE1234F1Z5')
..feed()
..labeled('Invoice', 'INV-1024')
..dateTime()
..divider()
..item('Basmati Rice 5kg', qty: 2, rate: 250) // amount auto = 500
..item('Cooking Oil 1L', qty: 1, rate: 180)
..item('Delivery', amount: 40) // qty/rate optional
..divider()
..totals(
subtotal: 720,
taxes: {'CGST 2.5%': 18, 'SGST 2.5%': 18},
total: 756,
tendered: 1000, // change auto-computed
)
..feed()
..center('Thank you! Visit again.')
..qr('https://pay.example/inv/1024');
await printer.printReceipt(receipt, openDrawer: true);
What you can add: title / text / center / right (styled lines),
labeled (left–right rows), item (auto qty × rate), totals (subtotal,
taxes/discounts, grand total, tendered + auto change), divider, feed,
qr, barcode, image (logo), and dateTime. Use
Receipt(itemStyle: ReceiptItemStyle.nameAmount) for simple name-and-price
bills. The item-name column always flexes to the paper width; widen the numeric
columns for big numbers with Receipt(qtyWidth: …, rateWidth: …, amountWidth: …).
Preview it on screen with ReceiptPreview(template: receipt.build()).
For fixed, reusable layouts driven by a data map, use the template engine and
PrebuiltTemplates (below) instead.
The workflow: connect → template → preview → print #
Connect a printer in one line (drop-in UI) #
The SDK ships a ready-made PrinterConnectPage that handles Bluetooth
permissions, turning Bluetooth on, live BLE scanning (with signal strength),
paired Bluetooth Classic devices, network auto-discovery, and connecting — with
friendly empty states. Just push it:
final printer = ThermalPrinter();
await Navigator.of(context).push(MaterialPageRoute(
builder: (_) => PrinterConnectPage(
printer: printer,
onConnected: (device) => Navigator.pop(context), // now connected & ready
),
));
// printer is connected — start printing
await printer.printTemplate(PrebuiltTemplates.retailInvoice(), data: data);
⚠️ Bluetooth won't find anything without permissions. Add them to your app (see Required native permissions) — the widget requests them at runtime, but they must be declared in the manifest/plist too.
Or connect programmatically #
final printer = ThermalPrinter(profile: PrinterProfile.mm80());
// Network auto-discovery — no IP typing
final found = await const NetworkPrinterDiscovery().discover();
await printer.connectDevice(found.first, autoReconnect: true);
// Preview before printing
ReceiptPreview(template: PrebuiltTemplates.retailInvoice(), data: data);
// Print
await printer.printTemplate(PrebuiltTemplates.retailInvoice(), data: data);
Industry quick-starts #
Each prebuilt template documents the exact data keys it expects. Fill them at
print time — the layout is reusable.
🛒 Retail (itemized bill with GST + change due) #
await printer.printTemplate(PrebuiltTemplates.retailInvoice(), data: {
'shopName': 'Ravi Traders', 'shopAddress': 'Jaipur',
'gstin': '08ABCDE1234F1Z5', 'invoiceNo': 'INV-1042',
'items': [
{'name': 'Cotton Shirt', 'qty': '2', 'rate': '499', 'amount': '998'},
{'name': 'Socks', 'qty': '3', 'rate': '99', 'amount': '297'},
],
'subtotal': '1295.00', 'cgst': '32.37', 'sgst': '32.37',
'total': '1359.74', 'tendered': '1500.00',
'receiptUrl': 'https://shop.example/r/1042',
});
🍽️ Restaurant (Kitchen Order Ticket) #
await kitchenPrinter.printTemplate(PrebuiltTemplates.restaurantKot(), data: {
'tableNo': '7', 'orderNo': 'A-231', 'waiter': 'Suresh',
'items': [{'qty': '2', 'name': 'Paneer Tikka'}, {'qty': '1', 'name': 'Naan'}],
'instructions': 'Less spicy, no onion',
});
📦 Logistics (shipping label with COD) #
await printer.printTemplate(PrebuiltTemplates.shippingLabel(), data: {
'carrier': 'SwiftShip', 'awb': '7789123456',
'senderName': 'Ravi Traders', 'senderAddr1': 'MI Road', 'senderAddr2': 'Jaipur',
'senderPhone': '9876543210',
'receiverName': 'Anita Verma', 'receiverAddr1': 'Sector 21',
'receiverAddr2': 'Gurugram', 'receiverPhone': '9812345678',
'weight': '1.2 kg', 'cod': '2723.70',
'trackingUrl': 'https://swiftship.example/t/7789123456',
});
💊 Pharmacy (dosage + expiry warning) #
await printer.printTemplate(PrebuiltTemplates.pharmacyLabel(), data: {
'pharmacyName': 'City Care Pharmacy', 'patientName': 'Mohit Sharma',
'rxNo': 'RX-5521', 'drugName': 'Amoxicillin 500mg',
'dosage': '1 capsule, 3 times a day after meals',
'quantity': '21 capsules', 'batch': 'B22F9', 'expiry': '11/2027',
});
Also available: parkingToken, bankMiniStatement, warehouseProductLabel,
queueToken.
Build your own template #
final t = TemplateBuilder('My Receipt')
.text('{{shopName}}', align: 1, bold: true, fontHeight: 2)
.divider()
.table(const [
TableColumn('Item', key: 'name'),
TableColumn('Amt', width: 9, align: 2, key: 'amount'),
], rowsKey: 'items')
.taxBreakdown(totalValue: '{{total}}', tenderedValue: '{{cash}}',
currencySymbol: '₹')
.qrCode('{{url}}')
.build();
Templates serialize to JSON (t.toJsonString() / PrintTemplate.fromJsonString)
so you can design once and store/ship them.
Connectivity features #
// Multiple named printers
final mgr = PrinterManager();
await mgr.add('receipt', (p) => p.connectNetwork('192.168.1.50'));
await mgr.add('kitchen', (p) => p.connectBluetooth('AA:BB:CC:DD:EE:FF'));
await mgr.printTemplateOn('kitchen', kot, data: order);
// Real-time status
final s = await printer.queryStatus();
if (s.paperOut) { /* prompt to add paper */ }
// Offline queue — nothing lost if the link drops mid-shift
final queue = OfflinePrintQueue();
printer.useOfflineQueue(queue); // auto-flushes on reconnect
// Typed errors
try { await printer.printTemplate(t, data: d); }
on PaperOutException { /* ... */ }
on PrinterOfflineException { /* ... */ }
Platform support #
| Platform | Bluetooth Classic | Bluetooth LE | USB | Network |
|---|---|---|---|---|
| Android | ✅ | ✅ | opt-in³ | ✅ |
| iOS | ⚠️ MFi only¹ | ✅ | ❌ | ✅ |
| Windows | ❌ | ✅ | opt-in³ | ✅ |
| Linux | ❌ | ✅ | opt-in³ | ✅ |
| macOS | ❌ | ✅ | opt-in³ | ✅ |
The public API is identical across platforms; an unsupported transport throws
UnsupportedTransportException.
¹ iOS Bluetooth requires MFi-certified printers via the External Accessory
framework; use Network for non-MFi printers on iOS.
³ USB is not bundled: the common usb_serial plugin is unmaintained and
breaks modern Android builds (AGP 8/9). The connectUSB() API is preserved but
inert until you wire a maintained USB plugin (e.g. quick_usb) into your app.
Most USB thermal printers on desktop are reachable via the OS print queue or a
virtual COM port instead.
Required native permissions #
Android — add to android/app/src/main/AndroidManifest.xml, above
<application> (without these, Bluetooth scans return nothing):
<!-- Android 12+ -->
<uses-permission android:name="android.permission.BLUETOOTH_SCAN"/>
<uses-permission android:name="android.permission.BLUETOOTH_CONNECT"/>
<!-- Android 11 and below -->
<uses-permission android:name="android.permission.BLUETOOTH" android:maxSdkVersion="30"/>
<uses-permission android:name="android.permission.BLUETOOTH_ADMIN" android:maxSdkVersion="30"/>
<!-- BLE scanning needs location on older Android -->
<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION"/>
iOS — add to ios/Runner/Info.plist:
<key>NSBluetoothAlwaysUsageDescription</key>
<string>Connect to Bluetooth thermal printers.</string>
PrinterConnectPage (and PrinterScanner.ensureBluetoothPermissions()) request
these at runtime; the manifest/plist entries above are still required.
Example app #
The example/ app is a complete workflow: a printer picker (with
auto-discovery and live status), a gallery of the industry templates, a
live receipt preview, and one-tap printing — with licensing handled
internally and a free-mode quota banner.
cd example
flutter create . # generate platform folders the first time
flutter run
License #
See LICENSE. This SDK includes an optional online activation backend; the
owner-only setup (Firebase + issuing keys) lives in OWNER_SETUP_PRIVATE.md
(git-ignored). Publishing steps are in PUBLISHING.md.