Costruire il documento PDF multipagina con MultiPage
Ora il cuore del tutorial. Creiamo un servizio InvoicePdfService che restituisce i byte del PDF (Uint8List): questa firma è perfetta perché è esattamente ciò che si aspettano Printing.layoutPdf, PdfPreview e File.writeAsBytes.
Punti chiave:
pw.MultiPage spezza automaticamente il contenuto su più pagine quando build restituisce una lista di widget troppo alta: ideale per fatture con molte righe.
header e footer sono callback invocate per ogni pagina e ricevono un pw.Context da cui leggere pageNumber e pagesCount.
pw.TableHelper.fromTextArray genera rapidamente una tabella con intestazione, allineamenti e stili per cella (in passato era pw.Table.fromTextArray, oggi deprecato).
- Tutte le misure si esprimono in punti PDF (1 punto = 1/72 di pollice); usa le costanti
PdfPageFormat.cm e PdfPageFormat.mm per ragionare in unità familiari.
Attenzione all'import: i widget del PDF vanno importati con il prefisso pw per non entrare in conflitto con quelli di Flutter.
// lib/services/invoice_pdf_service.dart
import 'dart:typed_data';
import 'package:intl/intl.dart';
import 'package:pdf/pdf.dart';
import 'package:pdf/widgets.dart' as pw;
import '../models/invoice.dart';
class InvoicePdfService {
final _currency = NumberFormat.currency(locale: 'it_IT', symbol: '€');
final _dateFormat = DateFormat('dd/MM/yyyy', 'it_IT');
Future<Uint8List> build(Invoice invoice) async {
final doc = pw.Document(title: 'Fattura ${invoice.number}');
doc.addPage(
pw.MultiPage(
pageFormat: PdfPageFormat.a4,
margin: const pw.EdgeInsets.all(32),
header: (context) => _header(invoice),
footer: (context) => pw.Container(
alignment: pw.Alignment.centerRight,
child: pw.Text(
'Pagina ${context.pageNumber} di ${context.pagesCount}',
style: const pw.TextStyle(fontSize: 9, color: PdfColors.grey600),
),
),
build: (context) => [
_customerBox(invoice),
pw.SizedBox(height: 24),
_itemsTable(invoice),
pw.SizedBox(height: 16),
_totals(invoice),
],
),
);
return doc.save();
}
pw.Widget _header(Invoice invoice) => pw.Column(
crossAxisAlignment: pw.CrossAxisAlignment.start,
children: [
pw.Row(
mainAxisAlignment: pw.MainAxisAlignment.spaceBetween,
children: [
pw.Text('Acme S.r.l.',
style: pw.TextStyle(
fontSize: 20, fontWeight: pw.FontWeight.bold)),
pw.Column(
crossAxisAlignment: pw.CrossAxisAlignment.end,
children: [
pw.Text('Fattura n. ${invoice.number}'),
pw.Text(_dateFormat.format(invoice.date),
style: const pw.TextStyle(color: PdfColors.grey700)),
],
),
],
),
pw.Divider(thickness: 1, color: PdfColors.grey400),
],
);
pw.Widget _customerBox(Invoice invoice) => pw.Container(
padding: const pw.EdgeInsets.all(12),
decoration: pw.BoxDecoration(
border: pw.Border.all(color: PdfColors.grey400),
borderRadius: pw.BorderRadius.circular(6),
),
child: pw.Column(
crossAxisAlignment: pw.CrossAxisAlignment.start,
children: [
pw.Text('Destinatario',
style: pw.TextStyle(
fontSize: 10,
fontWeight: pw.FontWeight.bold,
color: PdfColors.grey700)),
pw.SizedBox(height: 4),
pw.Text(invoice.customerName),
pw.Text(invoice.customerAddress),
],
),
);
pw.Widget _itemsTable(Invoice invoice) => pw.TableHelper.fromTextArray(
headers: ['Descrizione', 'Q.tà', 'Prezzo', 'Totale'],
data: invoice.items
.map((item) => [
item.description,
'${item.quantity}',
_currency.format(item.unitPrice),
_currency.format(item.total),
])
.toList(),
headerStyle: pw.TextStyle(fontWeight: pw.FontWeight.bold),
headerDecoration: const pw.BoxDecoration(color: PdfColors.grey300),
cellAlignments: {
0: pw.Alignment.centerLeft,
1: pw.Alignment.center,
2: pw.Alignment.centerRight,
3: pw.Alignment.centerRight,
},
columnWidths: {
0: const pw.FlexColumnWidth(4),
1: const pw.FlexColumnWidth(1),
2: const pw.FlexColumnWidth(2),
3: const pw.FlexColumnWidth(2),
},
);
pw.Widget _totals(Invoice invoice) => pw.Align(
alignment: pw.Alignment.centerRight,
child: pw.SizedBox(
width: 220,
child: pw.Column(
children: [
_totalRow('Imponibile', _currency.format(invoice.subtotal)),
_totalRow('IVA ${(invoice.vatRate * 100).toStringAsFixed(0)}%',
_currency.format(invoice.vat)),
pw.Divider(),
_totalRow('Totale', _currency.format(invoice.total), bold: true),
],
),
),
);
pw.Widget _totalRow(String label, String value, {bool bold = false}) {
final style = pw.TextStyle(
fontWeight: bold ? pw.FontWeight.bold : pw.FontWeight.normal,
fontSize: bold ? 14 : 11,
);
return pw.Padding(
padding: const pw.EdgeInsets.symmetric(vertical: 2),
child: pw.Row(
mainAxisAlignment: pw.MainAxisAlignment.spaceBetween,
children: [pw.Text(label, style: style), pw.Text(value, style: style)],
),
);
}
}
Risultato atteso
Il servizio compila e `build(invoice)` restituisce un `Uint8List` non vuoto contenente il PDF della fattura.