背景:用一个三方pdf库导出一定格式的数据,同时需要在App内页面上预览这些导出的数据,最好是相同格式布局的,而pdf库里的各种组件和flutter的widget并没有对应关系,无法直接展示。那么写两份相似逻辑的代码显得很信球,有什么办法解决这个痛点吗?答案是有的: “构建器模式”(Builder Pattern)
pw.Document 是 pdf 包中的类,它用于生成 PDF 文件,并不能直接展示在应用的页面上。要在 Flutter 应用中展示这些数据,PDF 文档和普通的 UI 组件(如 Widget)之间的结构差异较大,无法直接重用 pw.Document 的布局逻辑。
但是,为了减少代码的重复性,您可以通过提取布局逻辑,将生成 PDF 和在应用内预览逻辑分离。这样同一套逻辑既可以生成 PDF,又可以用于 UI 展示。以下是具体的实现思路:
1. 提取通用布局逻辑
您可以将布局提取为一个函数或方法,返回相同的数据结构,无论是用于 PDF 还是用于页面上的 UI 组件。
2. 创建布局方法
List<dynamic> buildDataLayout(dynamic builder) {
return [
builder.buildText('数据统计', fontSize: 20),
builder.buildText(
'经度:${position?.longitude} 纬度:${position?.latitude} 时间:${DateFormat('yyyy-MM-dd HH:mm:ss').format(DateTime.now())}',
fontSize: 20),
builder.buildTable(
headers: ['字段1', '字段2', '字段3', '字段4', '字段5', '字段6', '字段7', '字段8'],
rows: mainDataItems.map((e) {
return [
builder.buildText(e!.typeName ?? ''),
builder.buildText(e.numName ?? ''),
builder.buildText(e!.taskName ?? ''),
builder.buildText(e!.positionName ?? ''),
e!.imageData != null
? builder.buildImage(e.imageData)
: builder.buildText('No Image'),
builder.buildText(e!.partName ?? ''),
builder.buildText(e!.partStatus ?? ''),
builder.buildText(e!.partLocation ?? ''),
];
}).toList(),
),
];
}
3. 为 PDF 创建一个 PDFBuilder
class PDFBuilder {
final pw.Font ttfFont;
PDFBuilder(this.ttfFont);
pw.Widget buildText(String text, {double fontSize = 14}) {
return pw.Text(text, style: pw.TextStyle(fontSize: fontSize, font: ttfFont));
}
pw.Widget buildTable({required List<String> headers, required List<List<dynamic>> rows}) {
return pw.Table(
border: pw.TableBorder.all(),
children: [
pw.TableRow(
children: headers.map((header) => buildText(header)).toList(),
),
...rows.map((row) => pw.TableRow(children: row)),
],
);
}
pw.Widget buildImage(Uint8List imageData) {
return pw.Container(
height: 200,
width: 200,
child: pw.Image(pw.MemoryImage(imageData)),
);
}
}
4. 使用 PDFBuilder 生成 PDF
_generateReportPDF1() async {
final fontBytes = await rootBundle.load('assets/simhei.ttf');
final ttfFont = pw.Font.ttf(fontBytes);
final pdf = pw.Document();
if (mainDataItems.isEmpty) {
"没有数据".iToast();
return;
}
PDFBuilder pdfBuilder = PDFBuilder(ttfFont);
List<dynamic> layout = buildDataLayout(pdfBuilder);
pdf.addPage(
pw.Page(
build: (pw.Context context) {
return pw.Column(children: layout.cast<pw.Widget>());
},
),
);
// 保存PDF文件的逻辑保持不变
}
5. 为 Flutter UI 创建一个 FlutterBuilder
class FlutterBuilder {
Widget buildText(String text, {double fontSize = 14}) {
return Text(text, style: TextStyle(fontSize: fontSize));
}
Widget buildTable({required List<String> headers, required List<List<dynamic>> rows}) {
return Table(
border: TableBorder.all(),
children: [
TableRow(
children: headers.map((header) => buildText(header)).toList(),
),
...rows.map((row) => TableRow(children: row.cast<Widget>())),
],
);
}
Widget buildImage(Uint8List imageData) {
return Container(
height: 200,
width: 200,
child: Image.memory(imageData),
);
}
}
6. 在 Flutter 页面中使用 FlutterBuilder
class DataPreviewScreen extends StatelessWidget {
@override
Widget build(BuildContext context) {
if (mainDataItems.isEmpty) {
return Center(child: Text("没有数据"));
}
FlutterBuilder flutterBuilder = FlutterBuilder();
List<dynamic> layout = buildDataLayout(flutterBuilder);
return Scaffold(
appBar: AppBar(title: Text('数据预览')),
body: Padding(
padding: const EdgeInsets.all(8.0),
child: Column(
children: layout.cast<Widget>(),
),
),
);
}
}
总结
通过提取布局逻辑并为 PDF 和 Flutter UI 创建不同的构建器,您可以在不重复代码的情况下复用相同的数据布局。这种方式既能够生成 PDF,也可以在应用内进行预览。