Anyone who has built a retail POS, restaurant billing system, or e-commerce application in Flutter knows that document generation is deceptively c
omplex.
Rendering an A4 invoice on desktop or mobile is straightforward with pdf and printing. However, the moment you need to support 58mm and 80mm Bluetooth thermal receipt printers, several common issues arise:
- Thermal Head Dithering: Subtle greys or soft colors render as blurry, illegible dot matrices on standard 203 DPI thermal printers.
- Dynamic Height Calculation: Thermal receipt paper comes on continuous rolls. Fixed A4 canvas constraints result in feet of blank trailing paper or cut-off totals.
- Missing Glyphs & OS Dependencies: Default device fonts behave unpredictably across iOS, Android, macOS, and Windows.
In this guide, let's break down how to architect a decoupled Flutter invoice engine that dynamically adapts across A4, 80mm, and 58mm formats.
The Core Architecture
To support hot-swappable formats without duplicating rendering logic, decouple your data structure from the canvas drawer using strongly typed models:
class InvoiceItem {
final String description;
final int quantity;
final double unitPrice;
final double taxRate;
InvoiceItem({
required this.description,
required this.quantity,
required this.unitPrice,
this.taxRate = 0.0,
});
double get subtotal => quantity * unitPrice;
double get total => subtotal + (subtotal * taxRate);
}
*Solving Font Inconsistencies with Embedded Glyphs
*
Never rely on system-default fonts when generating PDFs across platforms. Load a clean, modern font like Inter directly from assets via rootBundle before generating document bytes:
import 'package:flutter/services.dart';
import 'package:pdf/pdf.dart';
import 'package:pdf/widgets.dart' as pw;
Future<pw.Font> loadCustomFont() async {
final fontData = await rootBundle.load('assets/fonts/Inter-Regular.ttf');
return pw.Font.ttf(fontData);
}
*Dynamic Format Switching & Pure-Black Contrast Mode
*
When targeting thermal rolls (58mm/80mm), switch to a high-contrast monochrome palette (#000000 text, borders, and dividers) and adjust page dimensions dynamically:
enum ReceiptFormat { a4, pos80mm, pos58mm }
PdfPageFormat getPageFormat(ReceiptFormat format) {
switch (format) {
case ReceiptFormat.pos58mm:
// 58mm roll width (approx 58 * PdfPageFormat.mm) with dynamic continuous height
return const PdfPageFormat(58 * PdfPageFormat.mm, double.infinity,
marginAll: 4 * PdfPageFormat.mm);
case ReceiptFormat.pos80mm:
// 80mm roll width
return const PdfPageFormat(80 * PdfPageFormat.mm, double.infinity,
marginAll: 6 * PdfPageFormat.mm);
case ReceiptFormat.a4:
default:
return PdfPageFormat.a4;
}
}
Constructing Format-Specific Layouts
For A4, standard multi-column tables work well. For 58mm POS rolls, space is tight, so stack item descriptions above quantity and pricing rather than forcing them into narrow multi-column tables:
pw.Widget buildItemRow(InvoiceItem item, ReceiptFormat format, pw.Font font) {
final isCompact = format == ReceiptFormat.pos58mm;
if (isCompact) {
return pw.Column(
crossContent: pw.CrossAxisAlignment.start,
children: [
pw.Text(item.description, style: pw.TextStyle(font: font, fontSize: 8, fontWeight: pw.FontWeight.bold)),
pw.Row(
mainAxisAlignment: pw.MainAxisAlignment.spaceBetween,
children: [
pw.Text('${item.quantity} x \$${item.unitPrice.toStringAsFixed(2)}', style: pw.TextStyle(font: font, fontSize: 7)),
pw.Text('\$${item.total.toStringAsFixed(2)}', style: pw.TextStyle(font: font, fontSize: 8, fontWeight: pw.FontWeight.bold)),
],
),
pw.Divider(thickness: 0.5, color: PdfColors.black),
],
);
}
// Standard multi-column row for A4 / 80mm
return pw.Row(
children: [
pw.Expanded(flex: 3, child: pw.Text(item.description, style: pw.TextStyle(font: font, fontSize: 9))),
pw.Expanded(flex: 1, child: pw.Text('${item.quantity}', style: pw.TextStyle(font: font, fontSize: 9))),
pw.Expanded(flex: 1, child: pw.Text('\$${item.unitPrice.toStringAsFixed(2)}', style: pw.TextStyle(font: font, fontSize: 9))),
pw.Expanded(flex: 1, child: pw.Text('\$${item.total.toStringAsFixed(2)}', style: pw.TextStyle(font: font, fontSize: 9), textAlign: pw.TextAlign.right)),
],
);
}
Live Interactive Preview & Printing
Use the printing package’s PdfPreview widget to let users switch formats on the fly with real-time pinch-to-zoom, system sharing, and direct Bluetooth/USB printer dispatch:
import 'package:flutter/material.dart';
import 'package:printing/printing.dart';
class ReceiptPreviewScreen extends StatelessWidget {
final Uint8List pdfBytes;
const ReceiptPreviewScreen({Key? key, required this.pdfBytes}) : super(key: key);
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: const Text('Receipt Preview')),
body: PdfPreview(
build: (format) => pdfBytes,
canChangeOrientation: false,
canChangePageFormat: false,
allowPrinting: true,
allowSharing: true,
),
);
}
}
📦 Turnkey Plug-and-Play Solution
If you're building a commercial Flutter project and want a pre-built, production-ready module that includes:
Complete A4, 80mm, and 58mm layouts with dynamic page scaling
High-contrast pure-black thermal mode
Bundled Inter font assets with zero setup
Dynamic preview screen with zoom, native share, and print hooks
Clean models and integration docs
You can grab the full turnkey source code here:
👉 Flutter Multi-Format Receipt & Invoice Engine on Gumroad
How are you currently handling thermal receipt generation in your Flutter applications? Let's discuss in the comments below!
Top comments (0)