Skip to content

Flutter Integration Guide

papermint-mobile provides high-performance C foreign function interface (FFI) bindings for Flutter apps on Android and iOS.

Instead of relying on fragile pure-Dart receipt packages that break on complex table layouts, Arabic shaping, or StarPRNT dialects, Papermint runs a compiled Rust engine that formats receipts and compiles ESC/POS or StarPRNT wire bytes in ~10 microseconds, returning a zero-copy byte buffer (Uint8List) ready for Bluetooth, BLE, or TCP transmission.


β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚ Flutter App (Dart UI / State) β”‚
β”‚ β€’ Defines receipt ticket (Map<String, dynamic>) β”‚
β”‚ β€’ Calls Papermint.compile(ticket) β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
β”‚ dart:ffi (Pointer / C-ABI)
β–Ό
β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚ libpapermint_mobile (Rust Engine) β”‚
β”‚ β€’ UAX #11 CJK & Arabic WPC1256/CP864 shaping β”‚
β”‚ β€’ Table column math & line wraps β”‚
β”‚ β€’ ESC/POS & StarPRNT dialect byte encoding (~10 Β΅s) β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
β”‚ Returns Uint8List raw bytes
β–Ό
β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚ Hardware Transports (Flutter Plugins / Sockets) β”‚
β”‚ β€’ Bluetooth Classic (SPP): print_bluetooth_thermal β”‚
β”‚ β€’ Bluetooth Low Energy: flutter_blue_plus β”‚
β”‚ β€’ Wi-Fi / Ethernet: dart:io Socket (Port 9100) β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜

You can integrate the native libraries using pre-compiled binaries (no Rust setup required) or by compiling from source.

Pre-compiled production binaries are attached to every GitHub Release. You do not need Rust or the Android NDK installed.

  1. Download papermint-mobile-android-v*.zip from the latest release.
  2. Extract the jniLibs directory directly into your Flutter Android folder:
    android/
    └── app/
    └── src/
    └── main/
    └── jniLibs/
    β”œβ”€β”€ arm64-v8a/libpapermint_mobile.so
    β”œβ”€β”€ armeabi-v7a/libpapermint_mobile.so
    └── x86_64/libpapermint_mobile.so
  1. Download papermint-mobile-ios-v*.zip from the latest release.
  2. Unzip PapermintMobile.xcframework and papermint.h.
  3. Open ios/Runner.xcworkspace in Xcode, and drag PapermintMobile.xcframework into Runner $\rightarrow$ General $\rightarrow$ Frameworks, Libraries, and Embedded Content (set to Embed & Sign).
  4. Copy papermint.h into ios/Runner/ and add #import "papermint.h" to your Runner-Bridging-Header.h.

If you are modifying the Rust receipt engine or building custom architectures:

Terminal window
# Inside crates/papermint-mobile
cargo ndk -t arm64-v8a -t armeabi-v7a -t x86_64 -o ../../android/app/src/main/jniLibs build --release
Terminal window
# Compile static libraries for device and simulator
cargo build --package papermint-mobile --target aarch64-apple-ios --release
cargo build --package papermint-mobile --target aarch64-apple-ios-sim --release

In Xcode under Runner $\rightarrow$ Build Phases $\rightarrow$ Link Binary With Libraries, add libpapermint_mobile.a.


In your Flutter app’s pubspec.yaml, add ffi:

Terminal window
flutter pub add ffi

Create lib/services/papermint.dart in your Flutter project. This single file provides type-safe Dart bindings, memory management, ticket compilation, and virtual SVG/HTML previews:

import 'dart:convert';
import 'dart:ffi';
import 'dart:io';
import 'dart:typed_data';
import 'package:ffi/ffi.dart';
/// Dialect selector for thermal printers.
enum PrinterDialect {
escpos, // 0: Epson, Rongta, Bixolon, Xprinter, POS-58/80
star, // 1: Star Micronics (TSP100, TSP650II, mC-Print)
}
// C-ABI Function Signatures
typedef _NativeCompileJson = Pointer<Uint8> Function(
Pointer<Utf8> json, Uint8 dialect, Pointer<Size> outLen);
typedef _DartCompileJson = Pointer<Uint8> Function(
Pointer<Utf8> json, int dialect, Pointer<Size> outLen);
typedef _NativeBytesFree = Void Function(Pointer<Uint8> ptr, Size len);
typedef _DartBytesFree = void Function(Pointer<Uint8> ptr, int len);
typedef _NativeCompileJsonStr = Pointer<Utf8> Function(Pointer<Utf8> json);
typedef _DartCompileJsonStr = Pointer<Utf8> Function(Pointer<Utf8> json);
typedef _NativeStringFree = Void Function(Pointer<Utf8> ptr);
typedef _DartStringFree = void Function(Pointer<Utf8> ptr);
/// High-performance Papermint printing service for Flutter.
class Papermint {
static final DynamicLibrary _lib = () {
if (Platform.isAndroid) {
return DynamicLibrary.open('libpapermint_mobile.so');
} else if (Platform.isIOS || Platform.isMacOS) {
return DynamicLibrary.process(); // Statically linked into Runner
} else if (Platform.isWindows) {
return DynamicLibrary.open('papermint_mobile.dll');
} else if (Platform.isLinux) {
return DynamicLibrary.open('libpapermint_mobile.so');
}
throw UnsupportedError('Unsupported platform for Papermint');
}();
static final _compileJson =
_lib.lookupFunction<_NativeCompileJson, _DartCompileJson>('papermint_compile_json');
static final _bytesFree =
_lib.lookupFunction<_NativeBytesFree, _DartBytesFree>('papermint_bytes_free');
static final _compileSvg =
_lib.lookupFunction<_NativeCompileJsonStr, _DartCompileJsonStr>('papermint_compile_json_svg');
static final _compileHtml =
_lib.lookupFunction<_NativeCompileJsonStr, _DartCompileJsonStr>('papermint_compile_json_html');
static final _stringFree =
_lib.lookupFunction<_NativeStringFree, _DartStringFree>('papermint_string_free');
/// Compiles a JSON ticket map into raw thermal printer wire bytes.
/// Execution time is typically 10-20 microseconds.
static Uint8List compileTicket(
Map<String, dynamic> ticket, {
PrinterDialect dialect = PrinterDialect.escpos,
}) {
final jsonStr = jsonEncode(ticket);
final jsonUtf8 = jsonStr.toNativeUtf8();
final outLenPtr = calloc<Size>();
try {
final dialectCode = dialect == PrinterDialect.star ? 1 : 0;
final bytesPtr = _compileJson(jsonUtf8, dialectCode, outLenPtr);
final len = outLenPtr.value;
if (bytesPtr.address == 0 || len == 0) {
return Uint8List(0);
}
// Copy native bytes to Dart Uint8List
final result = Uint8List.fromList(bytesPtr.asTypedList(len));
// Safely release native heap allocation
_bytesFree(bytesPtr, len);
return result;
} finally {
calloc.free(jsonUtf8);
calloc.free(outLenPtr);
}
}
/// Generates a pixel-perfect SVG vector string for virtual receipt preview.
static String renderSvg(Map<String, dynamic> ticket) {
final jsonStr = jsonEncode(ticket);
final jsonUtf8 = jsonStr.toNativeUtf8();
try {
final strPtr = _compileSvg(jsonUtf8);
if (strPtr.address == 0) return '';
final result = strPtr.toDartString();
_stringFree(strPtr);
return result;
} finally {
calloc.free(jsonUtf8);
}
}
/// Generates an HTML preview string of the receipt.
static String renderHtml(Map<String, dynamic> ticket) {
final jsonStr = jsonEncode(ticket);
final jsonUtf8 = jsonStr.toNativeUtf8();
try {
final strPtr = _compileHtml(jsonUtf8);
if (strPtr.address == 0) return '';
final result = strPtr.toDartString();
_stringFree(strPtr);
return result;
} finally {
calloc.free(jsonUtf8);
}
}
}

You can format receipts using standard Dart maps:

final ticket = {
"paper_width": "80mm", // or "58mm"
"title": "AL-NAKHEEL LUXURY CAFE",
"subtitle": "Order #4092",
"address": "King Fahd Rd, Riyadh 12214",
"metadata": [
{"label": "Date", "value": "2026-09-15 14:30"},
{"label": "Cashier", "value": "Fahad Al-Otaibi"}
],
"items": [
{"qty": "1x", "description": "Arabic Cardamom Coffee", "price": "18.00", "total": "18.00"},
{"qty": "2x", "description": "Saffron Milk Cake", "price": "24.00", "total": "48.00"},
{"qty": "1x", "description": "Sparkling Water", "price": "6.00", "total": "6.00"}
],
"divider_style": "=",
"totals": [
{"label": "Subtotal", "value": "72.00 SAR"},
{"label": "VAT (15%)", "value": "10.80 SAR"},
{"label": "TOTAL", "value": "82.80 SAR"}
],
"qr": "https://zatca.gov.sa/einvoice/verify?id=4092",
"barcode": "40928280",
"footer": "Thank you for visiting! / Ψ΄ΩƒΨ±Ψ§Ω‹ Ω„Ψ²ΩŠΨ§Ψ±ΨͺΩƒΩ…",
"cut_mode": "full",
"open_drawer": true,
"beep": 1
};
// Generate binary ESC/POS wire bytes in ~10 microseconds
final Uint8List wireBytes = Papermint.compileTicket(ticket, dialect: PrinterDialect.escpos);

A. Wi-Fi / Ethernet LAN (Native Dart Socket β€” Port 9100)

Section titled β€œA. Wi-Fi / Ethernet LAN (Native Dart Socket β€” Port 9100)”

No third-party packages required! Dart’s built-in dart:io sends raw bytes directly to network thermal printers:

import 'dart:io';
import 'dart:typed_data';
Future<void> printViaNetwork({
required String ipAddress,
int port = 9100,
required Uint8List bytes,
}) async {
final socket = await Socket.connect(ipAddress, port, timeout: const Duration(seconds: 5));
try {
socket.add(bytes);
await socket.flush();
} finally {
await socket.close();
}
print("βœ… Receipt printed successfully via LAN Port 9100!");
}

For Android mobile POS devices and budget thermal printers (e.g. POS-58, POS-80, PT-210), use print_bluetooth_thermal:

Terminal window
flutter pub add print_bluetooth_thermal
import 'package:print_bluetooth_thermal/print_bluetooth_thermal.dart';
Future<void> printViaBluetoothClassic({
required String macAddress,
required Uint8List bytes,
}) async {
final isConnected = await PrintBluetoothThermal.connect(macPrinterAddress: macAddress);
if (!isConnected) {
throw Exception("Failed to connect to Bluetooth printer");
}
// Stream raw bytes directly to the RFCOMM socket
final result = await PrintBluetoothThermal.writeBytes(bytes.toList());
print("βœ… Printed over Bluetooth Classic: $result");
}

For portable printers operating over BLE (e.g. Star SM-L200, Zebra, Rongta BLE), use flutter_blue_plus:

Terminal window
flutter pub add flutter_blue_plus
import 'dart:typed_data';
import 'package:flutter_blue_plus/flutter_blue_plus.dart';
Future<void> printViaBle({
required BluetoothDevice device,
required Guid serviceUuid,
required Guid characteristicUuid,
required Uint8List bytes,
}) async {
await device.connect();
// Discover GATT services and target write characteristic
final services = await device.discoverServices();
final service = services.firstWhere((s) => s.uuid == serviceUuid);
final char = service.characteristics.firstWhere((c) => c.uuid == characteristicUuid);
// Negotiate higher MTU if supported (up to 512 bytes)
if (Platform.isAndroid) {
await device.requestMtu(512);
}
// Chunk bytes to respect BLE packet size (default 244 or MTU - 3)
const chunkSize = 200;
for (var i = 0; i < bytes.length; i += chunkSize) {
final end = (i + chunkSize < bytes.length) ? i + chunkSize : bytes.length;
final chunk = bytes.sublist(i, end);
await char.write(chunk, withoutResponse: false);
}
print("βœ… Printed successfully over BLE!");
}

Before printing to physical paper, render the exact receipt output on screen using flutter_svg:

Terminal window
flutter pub add flutter_svg
import 'package:flutter/material.dart';
import 'package:flutter_svg/flutter_svg.dart';
import 'services/papermint.dart';
class ReceiptPreviewScreen extends StatelessWidget {
final Map<String, dynamic> ticket;
const ReceiptPreviewScreen({super.key, required this.ticket});
@override
Widget build(BuildContext context) {
// Generate virtual vector preview in microseconds
final String svgString = Papermint.renderSvg(ticket);
return Scaffold(
appBar: AppBar(title: const Text('Receipt Preview')),
body: Center(
child: SingleChildScrollView(
padding: const Duration(milliseconds: 16) != null
? const EdgeInsets.all(16.0)
: EdgeInsets.zero,
child: Container(
decoration: BoxDecoration(
color: Colors.white,
boxShadow: [
BoxShadow(
color: Colors.black.withOpacity(0.15),
blurRadius: 10,
offset: const Offset(0, 4),
)
],
),
child: SvgPicture.string(svgString),
),
),
),
);
}
}

Feature Legacy Dart Receipt Packages Papermint + dart:ffi
Compilation Speed 50–200 ms (GC-heavy) ~10–20 Β΅s (zero-copy Rust)
Dialect Parity ESC/POS only (often partial) Epson ESC/POS + StarPRNT
Arabic & CJK Shaping Broken / missing glyph shaper WPC1256 & CP864 hardware native
Virtual SVG/HTML Preview Not supported Full vector renderer included
Memory Footprint Heap allocations per command Single contiguous native buffer