evolute_printer_flutter 0.0.1
evolute_printer_flutter: ^0.0.1 copied to clipboard
A Flutter plugin for Evolute handheld devices with integrated thermal printer. Supports text, image, and QR code printing via the Evolute SDK AIDL service.
import 'dart:typed_data';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:evolute_printer_flutter/evolute_printer_flutter.dart';
void main() {
runApp(const EvolutePrinterExampleApp());
}
class EvolutePrinterExampleApp extends StatelessWidget {
const EvolutePrinterExampleApp({super.key});
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Evolute Printer Demo',
debugShowCheckedModeBanner: false,
theme: ThemeData(
colorScheme: ColorScheme.fromSeed(seedColor: const Color(0xFF1565C0)),
useMaterial3: true,
),
home: const PrinterDemoScreen(),
);
}
}
// =============================================================================
// Main Demo Screen
// =============================================================================
class PrinterDemoScreen extends StatefulWidget {
const PrinterDemoScreen({super.key});
@override
State<PrinterDemoScreen> createState() => _PrinterDemoScreenState();
}
class _PrinterDemoScreenState extends State<PrinterDemoScreen> {
final EvolutePrinter _printer = EvolutePrinter();
// Form controllers — mirrors the Java activity's input fields
final _plateController = TextEditingController();
final _descController = TextEditingController();
// UI state
bool _isConnecting = false;
bool _isPrinting = false;
PrinterStatus? _lastStatus;
final List<String> _logs = [];
// -----------------------------------------------------------------------
// Lifecycle
// -----------------------------------------------------------------------
@override
void initState() {
super.initState();
_connectPrinter();
}
@override
void dispose() {
_plateController.dispose();
_descController.dispose();
_disconnectPrinter();
super.dispose();
}
// -----------------------------------------------------------------------
// Printer lifecycle
// -----------------------------------------------------------------------
Future<void> _connectPrinter() async {
setState(() => _isConnecting = true);
_addLog('Connecting to printer service...');
try {
await _printer.connect();
_addLog('✅ Printer service connected');
} on PrinterException catch (e) {
_addLog('❌ Connect failed: ${e.message}');
_showSnackBar(e.message, isError: true);
} finally {
setState(() => _isConnecting = false);
}
}
Future<void> _disconnectPrinter() async {
try {
await _printer.disconnect();
} catch (_) {}
}
// -----------------------------------------------------------------------
// Print job — replicates Printdata() from EntryTicketScreen.java
// -----------------------------------------------------------------------
/// Full valet parking ticket print — exact replica of the Java Printdata() flow.
Future<void> _printValetTicket() async {
final plateNumber = _plateController.text.trim().toUpperCase();
final description = _descController.text.trim();
if (plateNumber.isEmpty) {
_showSnackBar('Please enter a plate number', isError: true);
return;
}
if (!_printer.isConnected) {
_showSnackBar('Printer not connected. Tap "Reconnect".', isError: true);
return;
}
setState(() => _isPrinting = true);
_addLog('--- Starting print job ---');
try {
final now = DateTime.now();
final inTime =
'${_pad(now.day)}/${_pad(now.month)}/${now.year} '
'${_pad(now.hour)}:${_pad(now.minute)}:${_pad(now.second)}';
final ticketNumber = 'TKT${now.millisecondsSinceEpoch}';
// ── Section 1: Header ──────────────────────────────────────────────
_addLog('Printing header...');
await _printer.setProperties(
alignment: PrintAlignment.center,
fontSize: PrintFontSize.normal,
);
await _printer.appendText('\nValet Parking Ticket No:');
await _printer.startPrinting();
// ── Section 2: Logo image ──────────────────────────────────────────
_addLog('Printing logo...');
await _printer.setProperties(
alignment: PrintAlignment.center,
fontSize: PrintFontSize.normal,
);
final Uint8List logoBytes = await _loadAssetBytes('assets/logo.png');
await _printer.printImage(logoBytes);
// ── Section 3: Ticket number & location ───────────────────────────
_addLog('Printing ticket info...');
await _printer.flushBuffer();
await _printer.setProperties(
alignment: PrintAlignment.center,
fontSize: PrintFontSize.normal,
);
await _printer.appendText('\n$ticketNumber');
await _printer.appendText('\n\nMANAGED BY');
await _printer.appendText('\nOMNYPARK MANAGEMENT SERVICES LLC');
await _printer.appendText('\n\nLocation : EMAAR BUSINESS PARK BUILDING 3');
await _printer.startPrinting();
// ── Section 4: Vehicle details ────────────────────────────────────
_addLog('Printing vehicle details...');
await _printer.flushBuffer();
await _printer.setProperties(
alignment: PrintAlignment.left,
fontSize: PrintFontSize.normal,
);
await _printer.appendText('\nIN TIME : $inTime');
await _printer.appendText('\nPLATE No : $plateNumber');
if (description.isNotEmpty) {
await _printer.appendText('\nDESC : $description');
}
await _printer.startPrinting();
// ── Section 5: Terms & conditions ─────────────────────────────────
_addLog('Printing terms...');
await _printer.flushBuffer();
await _printer.setProperties(
alignment: PrintAlignment.center,
fontSize: PrintFontSize.normal,
);
await _printer.appendText('\n_________________');
await _printer.appendText('\n\nVALET PARKING AGREEMENT');
await _printer.appendText('\nPLEASE READ');
await _printer.appendText('\n$_valetTerms');
await _printer.startPrinting();
// ── Section 6: Paper feed ─────────────────────────────────────────
_addLog('Feeding paper...');
await _printer.feedLines(9);
// ── Status check ──────────────────────────────────────────────────
final status = await _printer.getStatus();
setState(() => _lastStatus = status);
if (status.isSuccess) {
_addLog('✅ Print job completed successfully');
_showSnackBar('Ticket printed successfully!');
_plateController.clear();
_descController.clear();
} else {
_addLog('⚠️ Printer status: ${status.message}');
_showSnackBar(status.message, isError: true);
}
} on PrinterException catch (e) {
_addLog('❌ Print failed: ${e.message}');
_showSnackBar(e.message, isError: true);
} catch (e) {
_addLog('❌ Unexpected error: $e');
_showSnackBar('Unexpected error: $e', isError: true);
} finally {
setState(() => _isPrinting = false);
_addLog('--- Print job ended ---');
}
}
/// Minimal QR-only print — replicates Printdata1() from the Java activity.
Future<void> _printQRTicket() async {
final plateNumber = _plateController.text.trim().toUpperCase();
if (plateNumber.isEmpty) {
_showSnackBar('Please enter a plate number', isError: true);
return;
}
if (!_printer.isConnected) {
_showSnackBar('Printer not connected.', isError: true);
return;
}
setState(() => _isPrinting = true);
_addLog('--- Starting QR print job ---');
try {
final ticketNumber = 'TKT${DateTime.now().millisecondsSinceEpoch}';
final now = DateTime.now();
final inTime =
'${_pad(now.day)}/${_pad(now.month)}/${now.year} '
'${_pad(now.hour)}:${_pad(now.minute)}:${_pad(now.second)}';
await _printer.flushBuffer();
await _printer.setProperties(
alignment: PrintAlignment.center,
fontSize: PrintFontSize.normal,
);
await _printer.appendText('\nValet Parking Ticket No:');
await _printer.printQRCode(
data: ticketNumber,
width: QRWidth.inch2,
alignment: PrintAlignment.center,
);
await _printer.appendText('\n$ticketNumber');
await _printer.appendText('\n\nManaged by:');
await _printer.appendText('\nOMNYPARK MANAGEMENT SERVICES LLC');
await _printer.appendText('\n\nLocation : EMAAR BUSINESS PARK BUILDING 3');
await _printer.appendText('\n\nIN TIME : $inTime');
await _printer.appendText('\nPLATE No : $plateNumber');
await _printer.appendText('\n_________________');
await _printer.appendText('\n\nVALET PARKING AGREEMENT');
await _printer.appendText('\nPLEASE READ');
await _printer.appendText('\n$_valetTerms');
await _printer.startPrinting();
await _printer.feedLines(9);
final status = await _printer.getStatus();
setState(() => _lastStatus = status);
if (status.isSuccess) {
_addLog('✅ QR ticket printed successfully');
_showSnackBar('QR Ticket printed!');
_plateController.clear();
_descController.clear();
} else {
_addLog('⚠️ ${status.message}');
_showSnackBar(status.message, isError: true);
}
} on PrinterException catch (e) {
_addLog('❌ ${e.message}');
_showSnackBar(e.message, isError: true);
} finally {
setState(() => _isPrinting = false);
_addLog('--- QR print job ended ---');
}
}
// -----------------------------------------------------------------------
// Helpers
// -----------------------------------------------------------------------
Future<Uint8List> _loadAssetBytes(String assetPath) async {
final ByteData data = await rootBundle.load(assetPath);
return data.buffer.asUint8List();
}
String _pad(int n) => n.toString().padLeft(2, '0');
void _addLog(String message) {
setState(() {
_logs.add('[${TimeOfDay.now().format(context)}] $message');
if (_logs.length > 50) _logs.removeAt(0); // keep log bounded
});
}
void _showSnackBar(String message, {bool isError = false}) {
if (!mounted) return;
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text(message),
backgroundColor: isError ? Colors.red.shade700 : Colors.green.shade700,
behavior: SnackBarBehavior.floating,
),
);
}
// -----------------------------------------------------------------------
// Build
// -----------------------------------------------------------------------
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text('Evolute Printer Demo'),
backgroundColor: Theme.of(context).colorScheme.inversePrimary,
actions: [
// Connection status indicator
Padding(
padding: const EdgeInsets.only(right: 12),
child: _isConnecting
? const SizedBox(
width: 20,
height: 20,
child: CircularProgressIndicator(strokeWidth: 2),
)
: Icon(
_printer.isConnected ? Icons.print : Icons.print_disabled,
color: _printer.isConnected ? Colors.green : Colors.red,
),
),
],
),
body: SingleChildScrollView(
padding: const EdgeInsets.all(16),
child: Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
// ── Connection card ──────────────────────────────────────────
_ConnectionStatusCard(
isConnected: _printer.isConnected,
isConnecting: _isConnecting,
lastStatus: _lastStatus,
onReconnect: _connectPrinter,
),
const SizedBox(height: 16),
// ── Input form ───────────────────────────────────────────────
_InputFormCard(
plateController: _plateController,
descController: _descController,
),
const SizedBox(height: 16),
// ── Print actions ────────────────────────────────────────────
_PrintActionsCard(
isPrinting: _isPrinting,
isConnected: _printer.isConnected,
onPrintTicket: _printValetTicket,
onPrintQRTicket: _printQRTicket,
),
const SizedBox(height: 16),
// ── Log console ──────────────────────────────────────────────
_LogConsoleCard(logs: _logs),
],
),
),
);
}
}
// =============================================================================
// Sub-widgets
// =============================================================================
class _ConnectionStatusCard extends StatelessWidget {
final bool isConnected;
final bool isConnecting;
final PrinterStatus? lastStatus;
final VoidCallback onReconnect;
const _ConnectionStatusCard({
required this.isConnected,
required this.isConnecting,
required this.lastStatus,
required this.onReconnect,
});
@override
Widget build(BuildContext context) {
return Card(
child: Padding(
padding: const EdgeInsets.all(16),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text('Printer Service',
style: Theme.of(context).textTheme.titleMedium),
const SizedBox(height: 12),
Row(
children: [
Container(
width: 12,
height: 12,
decoration: BoxDecoration(
shape: BoxShape.circle,
color: isConnecting
? Colors.orange
: isConnected
? Colors.green
: Colors.red,
),
),
const SizedBox(width: 8),
Text(
isConnecting
? 'Connecting...'
: isConnected
? 'Connected'
: 'Disconnected',
style: TextStyle(
color: isConnecting
? Colors.orange
: isConnected
? Colors.green
: Colors.red,
fontWeight: FontWeight.w600,
),
),
const Spacer(),
if (!isConnected && !isConnecting)
TextButton.icon(
onPressed: onReconnect,
icon: const Icon(Icons.refresh, size: 16),
label: const Text('Reconnect'),
),
],
),
if (lastStatus != null) ...[
const SizedBox(height: 8),
Text(
'Last status: ${lastStatus!.message}',
style: Theme.of(context).textTheme.bodySmall?.copyWith(
color: lastStatus!.isSuccess
? Colors.green
: Colors.red.shade700,
),
),
],
],
),
),
);
}
}
class _InputFormCard extends StatelessWidget {
final TextEditingController plateController;
final TextEditingController descController;
const _InputFormCard({
required this.plateController,
required this.descController,
});
@override
Widget build(BuildContext context) {
return Card(
child: Padding(
padding: const EdgeInsets.all(16),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text('Vehicle Details',
style: Theme.of(context).textTheme.titleMedium),
const SizedBox(height: 16),
TextField(
controller: plateController,
textCapitalization: TextCapitalization.characters,
decoration: const InputDecoration(
labelText: 'Plate Number *',
hintText: 'e.g. AB1234',
prefixIcon: Icon(Icons.directions_car),
border: OutlineInputBorder(),
),
),
const SizedBox(height: 12),
TextField(
controller: descController,
decoration: const InputDecoration(
labelText: 'Description (optional)',
hintText: 'e.g. Red sedan, scratch on door',
prefixIcon: Icon(Icons.notes),
border: OutlineInputBorder(),
),
),
],
),
),
);
}
}
class _PrintActionsCard extends StatelessWidget {
final bool isPrinting;
final bool isConnected;
final VoidCallback onPrintTicket;
final VoidCallback onPrintQRTicket;
const _PrintActionsCard({
required this.isPrinting,
required this.isConnected,
required this.onPrintTicket,
required this.onPrintQRTicket,
});
@override
Widget build(BuildContext context) {
return Card(
child: Padding(
padding: const EdgeInsets.all(16),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text('Print Actions',
style: Theme.of(context).textTheme.titleMedium),
const SizedBox(height: 4),
Text(
'Both flows replicate the original Java Printdata() methods',
style: Theme.of(context).textTheme.bodySmall,
),
const SizedBox(height: 16),
SizedBox(
width: double.infinity,
child: FilledButton.icon(
onPressed: (!isPrinting && isConnected) ? onPrintTicket : null,
icon: isPrinting
? const SizedBox(
width: 16,
height: 16,
child: CircularProgressIndicator(
strokeWidth: 2,
color: Colors.white,
),
)
: const Icon(Icons.print),
label: Text(isPrinting ? 'Printing...' : 'Print Valet Ticket'),
),
),
const SizedBox(height: 8),
SizedBox(
width: double.infinity,
child: OutlinedButton.icon(
onPressed: (!isPrinting && isConnected) ? onPrintQRTicket : null,
icon: const Icon(Icons.qr_code),
label: const Text('Print QR Ticket'),
),
),
],
),
),
);
}
}
class _LogConsoleCard extends StatelessWidget {
final List<String> logs;
const _LogConsoleCard({required this.logs});
@override
Widget build(BuildContext context) {
return Card(
child: Padding(
padding: const EdgeInsets.all(16),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
children: [
Text('Log', style: Theme.of(context).textTheme.titleMedium),
const Spacer(),
Text('${logs.length} entries',
style: Theme.of(context).textTheme.bodySmall),
],
),
const SizedBox(height: 8),
Container(
width: double.infinity,
height: 200,
padding: const EdgeInsets.all(8),
decoration: BoxDecoration(
color: Colors.grey.shade900,
borderRadius: BorderRadius.circular(8),
),
child: logs.isEmpty
? const Center(
child: Text('No logs yet',
style: TextStyle(color: Colors.grey)),
)
: ListView.builder(
reverse: true,
itemCount: logs.length,
itemBuilder: (_, i) {
final log = logs[logs.length - 1 - i];
final color = log.contains('❌')
? Colors.red.shade300
: log.contains('✅')
? Colors.green.shade300
: log.contains('⚠️')
? Colors.orange.shade300
: Colors.grey.shade300;
return Text(
log,
style: TextStyle(
color: color,
fontSize: 11,
fontFamily: 'monospace',
),
);
},
),
),
],
),
),
);
}
}
// =============================================================================
// Constants
// =============================================================================
const String _valetTerms =
'Omnypark reserves the right to move the valet vehicle to the designated '
'valet parking area depending on the availability of parking space without '
'any prior notice. Omnypark, Building Management or The Landlord will not '
'take any responsibility in case the parked vehicle being stolen or '
'vandalized by third party. The valet service of your vehicle is handled '
'by our valet drivers on your behalf at your own risk without any liability '
'on us. All valuable should be removed from the vehicle at the time of '
'valet service. In the event of losing this valet ticket your ID and the '
'car registration card will be required to claim your vehicle along with '
'the lost ticket payment as per the tariff.';