设计模式之:构建器模式(Builder Pattern)解决相似布局在不同场景下的需求(pdf预览+输出)

241 阅读2分钟

背景:用一个三方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,也可以在应用内进行预览。