DEV Community

Usama Razzaq
Usama Razzaq

Posted on

Building a Dynamic Multi-Format Invoice & Thermal POS Engine in Flutter (A4, 80mm, 58mm)

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:

  1. Thermal Head Dithering: Subtle greys or soft colors render as blurry, illegible dot matrices on standard 203 DPI thermal printers.
  2. 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.
  3. 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);
}
Enter fullscreen mode Exit fullscreen mode

*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);
}
Enter fullscreen mode Exit fullscreen mode

*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;
  }
}
Enter fullscreen mode Exit fullscreen mode

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)),
    ],
  );
}
Enter fullscreen mode Exit fullscreen mode

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,
      ),
    );
  }
}
Enter fullscreen mode Exit fullscreen mode

📦 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)