Flutter 鸿蒙适配版实战:printing PDF 打印插件在 HarmonyOS 上的接入与使用

库版本:printing 1.0.1(OpenHarmony 适配版)

适配仓库:https://atomgit.com/CPF-Flutter/fluttertpc_printing

验证环境:Flutter 鸿蒙 SDK 3.44.9-dev

设备鸿蒙 PC(OpenHarmony 6.1.1,API 24,arm64,2in1 形态)

在这里插入图片描述

在这里插入图片描述

在这里插入图片描述

在这里插入图片描述

在这里插入图片描述

在这里插入图片描述

一、环境搭建

Flutter 鸿蒙环境搭建请直接参考官方文档:Flutter 鸿蒙环境搭建指南

本章不重复展开,仅引用。搭建完成后,可在命令行执行 flutter doctor 确认环境就绪(鸿蒙版 Flutter SDK 默认支持 ohos 平台)。

二、应用背景

2.1 当前的应用场景与痛点

企业办公、发票开具、合同签署、报表导出等场景都离不开 PDF 文档的生成与打印。Flutter 应用在 Android 和 iOS 上可以借助 printing 插件轻松完成 PDF 创建、预览、打印和分享,但鸿蒙系统缺少对应的原生打印适配,开发者如果自行实现,需要:

  • 对接鸿蒙系统的 PDFKit(pdfService)和打印服务(print),API 较为底层;
  • 处理 PDF 文件写入临时目录、打印适配器回调、页面渲染等复杂流程;
  • 实现系统分享(ShareKit)将 PDF 发送给其他应用;
  • 将 PDF 页面光栅化为图片,用于应用内预览。

2.2 为什么需要这个库

printing 是 DavBfr 开源的 Flutter PDF 插件,Android 和 iOS 侧分别对接各自的打印框架。鸿蒙适配版(printing_ohos)在 OpenHarmony 平台上基于 ArkTS 重新实现了插件的原生层,调用鸿蒙 PDFKit 解析 PDF、调用系统打印服务完成打印、调用 ShareKit 实现 PDF 分享,让 Flutter 应用无需修改业务代码结构,即可把 PDF 生成与打印能力平滑带到鸿蒙设备上。

2.3 解决什么问题

一句话总结:为 Flutter 鸿蒙应用提供开箱即用的 PDF 文档生成、打印、预览和分享能力。具体包括:

  1. PDF 文档生成(配合 pdf 包创建文字、图片、表格、图表等丰富内容);
  2. 系统打印(调用鸿蒙打印服务,弹出打印对话框选择打印机);
  3. PDF 预览(PdfPreview 组件在应用内实时预览 PDF);
  4. PDF 分享(通过系统分享面板将 PDF 发送给其他应用);
  5. PDF 转图片(将 PDF 页面渲染为位图,用于缩略图或截图);
  6. 保存为文件(将 PDF 写入本地文件系统并用其他应用打开)。

三、功能介绍

功能说明适用场景
打印 PDFlayoutPdf() 调用系统打印对话框输出 PDF办公打印、发票打印、报表打印
列出打印机listPrinters() 枚举系统可用打印机指定打印机直连打印
选择打印机pickPrinter() 弹出打印机选择对话框用户交互式选择打印目标
直接打印directPrintPdf() 跳过 UI 直接向指定打印机发送自动化打印、固定打印机场景
分享 PDFsharePdf() 调用系统分享面板发送 PDF邮件发送、社交分享、文件传输
PDF 转图片raster() 将 PDF 页面渲染为位图流缩略图生成、页面预览
能力查询info() 返回当前平台支持的打印能力运行时判断功能可用性
PDF 预览PdfPreview 组件实时预览 PDF 文档应用内文档查看、打印前预览
保存文件配合 path_provider 将 PDF 写入本地离线保存、后续打开
字体加载fontFromAssetBundle() 从 asset 加载 TTF 字体中文文档、自定义字体
图片加载imageFromAssetBundle() / networkImage() 加载图片到 PDF文档插图、Logo 嵌入
PDF 创建配合 pdf 包的 Document / Page / MultiPage 等组件简历、发票、报告、证书等

四、使用方法

在这里插入图片描述

4.1 引入三方库

鸿蒙适配版需要通过 Git 依赖方式引入。在 pubspec.yaml 中添加:

dependencies:
  flutter:
    sdk: flutter

  # 鸿蒙适配版 PDF 打印插件
  printing_ohos:
    git:
      url: https://atomgit.com/oh-flutter/fluttertpc_printing.git
      path: printing/ohos    # 适配代码位于仓库 printing/ohos 子目录,必须指定
      ref: master            # 开发调试用分支;生产建议换成 tag 锁定版本

  # PDF 文档创建引擎(printing 依赖)
  pdf:

注意三点:
OpenHarmony 版本的适配代码在仓库的 printing/ohos 路径下,path 不能省略;引入后导入语句为 import ‘package:printing_ohos/printing.dart’;(包名为 printing_ohos,与 pub.dev 原库的 printing 不同名);PDF 文档创建依赖 pdf 包,需同时引入。

执行 flutter pub get 拉取依赖。

若工程同时存在 pub.dev 原库与鸿蒙适配版导致版本解析冲突,用 dependency_overrides 强制统一为鸿蒙适配版本:

dependency_overrides:
  printing:
    git:
      url: https://atomgit.com/oh-flutter/fluttertpc_printing.git
      path: printing/ohos
      ref: master

4.2 PDF 创建与打印 API

插件的核心能力是 PDF 文档创建与打印。首先用 pdf 包创建文档,然后调用 Printing 类的方法输出:

import 'package:pdf/pdf.dart';
import 'package:pdf/widgets.dart' as pw;
import 'package:printing_ohos/printing.dart';

// 创建 PDF 文档
final doc = pw.Document();
doc.addPage(
  pw.Page(
    pageFormat: PdfPageFormat.a4,
    build: (pw.Context context) {
      return pw.Center(
        child: pw.Text('Hello HarmonyOS',
          style: pw.TextStyle(fontSize: 40)),
      );
    },
  ),
);

layoutPdf() 调用系统打印对话框,用户可选择打印机并完成打印。onLayout 回调在用户切换纸张大小或方向时重新生成 PDF:

await Printing.layoutPdf(
  onLayout: (PdfPageFormat format) async => doc.save(),
);

sharePdf() 将 PDF 通过系统分享面板发送给其他应用,filename 为分享时的默认文件名:

await Printing.sharePdf(
  bytes: await doc.save(),
  filename: 'my-document.pdf',
);

listPrinters() 枚举系统已连接的打印机列表,返回 Printer 对象数组,每个对象包含打印机名称、是否默认等信息:

final List<Printer> printers = await Printing.listPrinters();
for (final printer in printers) {
  print('Printer: ${printer.name}, isDefault: ${printer.isDefault}');
}

directPrintPdf() 跳过系统打印对话框,直接向指定打印机发送 PDF。需先通过 listPrinters() 或 pickPrinter() 获取 Printer 对象:

final printers = await Printing.listPrinters();
if (printers.isNotEmpty) {
  await Printing.directPrintPdf(
    printer: printers.first,
    onLayout: (PdfPageFormat format) async => doc.save(),
  );
}

将 PDF 保存为本地文件,配合 path_provider 获取目录后用 OpenFile 打开:

import 'package:path_provider/path_provider.dart';
import 'package:open_file_ohos/open_file_ohos.dart';

final output = await getApplicationDocumentsDirectory();
final file = File('${output.path}/document.pdf');
await file.writeAsBytes(await doc.save());
await OpenFile.open(file.path);

运行效果:layoutPdf() 调用后弹出系统打印对话框,可选择打印机并完成打印;sharePdf() 调用后弹出系统分享面板,可选择邮件、文件管理等应用。

4.3 PDF 预览与光栅化 API

PdfPreview 是一个 Flutter Widget,可在应用内实时预览 PDF 文档,内置打印和分享按钮:

PdfPreview(
  maxPageWidth: 700,
  build: (format) => doc.save(),
);

raster() 将 PDF 文档的指定页面渲染为位图流,可用于生成缩略图或在 Canvas 中绘制:

await for (final page in Printing.raster(await doc.save(), pages: [0, 1], dpi: 72)) {
  final image = page.toImage();  // 得到 dart:ui Image
}

info() 查询当前平台支持的打印能力,返回 PrintingInfo 对象,各字段表示对应功能是否可用:

final printingInfo = await Printing.info();
// printingInfo.canPrint   → 是否支持打印
// printingInfo.canShare   → 是否支持分享
// printingInfo.canRaster  → 是否支持光栅化

运行效果:PdfPreview 组件在页面中显示 PDF 预览,支持翻页、缩放,右上角自带打印和分享图标按钮。raster() 返回的位图可直接显示在 Image 组件中。

4.4 字体与图片加载 API

fontFromAssetBundle() 从 asset 加载 TTF 字体文件,用于 PDF 中的文字渲染,中文文档必须加载中文 TTF 字体:

final ttf = await pw.fontFromAssetBundle('assets/fonts/SourceHanSans.ttf');

doc.addPage(
  pw.Page(
    build: (context) => pw.Text(
      '鸿蒙 PDF 打印测试',
      style: pw.TextStyle(font: ttf, fontSize: 24),
    ),
  ),
);

imageFromAssetBundle() 从 asset 加载图片,networkImage() 从网络 URL 下载图片,均可嵌入 PDF 页面:

// 从 asset 加载图片
final logo = await pw.imageFromAssetBundle('assets/logo.png');

// 从网络加载图片
final remoteImage = await pw.networkImage('https://example.com/banner.png');

doc.addPage(
  pw.Page(
    build: (context) => pw.Column(
      children: [
        pw.Image(logo, width: 200),
        pw.Image(remoteImage, width: 400),
      ],
    ),
  ),
);

运行效果:加载字体后 PDF 中的中文可正常显示(不会变成方块或空白);加载图片后 PDF 页面中可看到 Logo 和网络图片。

4.5 完整示例代码

以下是项目真实示例代码(demo/lib 目录),已在鸿蒙 PC(OpenHarmony 6.1.1,API 24,2in1 形态)上真机验证。示例提供 6 种 PDF 模板(简历、文档、发票、报告、日历、证书),通过 TabBar 切换,支持 PdfPreview 预览、打印、分享和保存为文件。

main.dart 入口文件:

import 'package:flutter/material.dart';

import 'app.dart';

void main() {
  runApp(const App());
}

class App extends StatelessWidget {
  const App({Key? key}) : super(key: key);

  
  Widget build(BuildContext context) {
    final scrollbarTheme = ScrollbarThemeData(
      thumbVisibility: WidgetStateProperty.all(true),
    );

    return MaterialApp(
      theme: ThemeData.light().copyWith(scrollbarTheme: scrollbarTheme),
      darkTheme: ThemeData.dark().copyWith(scrollbarTheme: scrollbarTheme),
      title: 'Flutter PDF Demo',
      home: const MyApp(),
    );
  }
}

data.dart 数据模型,存储用户输入的自定义数据:

class CustomData {
  const CustomData({
    this.name = '[your name]',
    this.testing = false,
  });

  final String name;

  final bool testing;
}

examples.dart 示例模板注册表,定义 6 种 PDF 模板及其构建函数:

import 'dart:async';
import 'dart:typed_data';

import 'package:pdf/pdf.dart';

import 'data.dart';
import 'examples/calendar.dart';
import 'examples/certificate.dart';
import 'examples/document.dart';
import 'examples/invoice.dart';
import 'examples/report.dart';
import 'examples/resume.dart';

const examples = <Example>[
  Example('RÉSUMÉ', 'resume.dart', generateResume),
  Example('DOCUMENT', 'document.dart', generateDocument),
  Example('INVOICE', 'invoice.dart', generateInvoice),
  Example('REPORT', 'report.dart', generateReport),
  Example('CALENDAR', 'calendar.dart', generateCalendar),
  Example('CERTIFICATE', 'certificate.dart', generateCertificate, true),
];

typedef LayoutCallbackWithData = Future<Uint8List> Function(
    PdfPageFormat pageFormat, CustomData data);

class Example {
  const Example(this.name, this.file, this.builder, [this.needsData = false]);

  final String name;

  final String file;

  final LayoutCallbackWithData builder;

  final bool needsData;
}

app.dart 核心页面,使用 TabBar 切换 6 种 PDF 模板,PdfPreview 实时预览,支持保存为文件并用其他应用打开:

import 'dart:async';
import 'dart:io';

import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:open_file_ohos/open_file_ohos.dart';
import 'package:path_provider/path_provider.dart';
import 'package:pdf/pdf.dart';
import 'package:pdf/widgets.dart' as pw;
import 'package:printing_ohos/printing.dart';
import 'package:url_launcher_platform_interface/url_launcher_platform_interface.dart';

import 'data.dart';
import 'examples.dart';

class MyApp extends StatefulWidget {
  const MyApp({Key? key}) : super(key: key);

  
  MyAppState createState() {
    return MyAppState();
  }
}

class MyAppState extends State<MyApp> with SingleTickerProviderStateMixin {
  int _tab = 0;
  TabController? _tabController;

  PrintingInfo? printingInfo;

  var _data = const CustomData();
  var _hasData = false;
  var _pending = false;

  
  void initState() {
    super.initState();
    _init();
  }

  Future<void> _init() async {
    final info = await Printing.info();

    _tabController = TabController(
      vsync: this,
      length: examples.length,
      initialIndex: _tab,
    );
    _tabController!.addListener(() {
      if (_tab != _tabController!.index) {
        setState(() {
          _tab = _tabController!.index;
        });
      }
      if (examples[_tab].needsData && !_hasData && !_pending) {
        _pending = true;
        askName(context).then((value) {
          if (value != null) {
            setState(() {
              _data = CustomData(name: value);
              _hasData = true;
              _pending = false;
            });
          }
        });
      }
    });

    setState(() {
      printingInfo = info;
    });
  }

  void _showPrintedToast(BuildContext context) {
    ScaffoldMessenger.of(context).showSnackBar(
      const SnackBar(
        content: Text('Document printed successfully'),
      ),
    );
  }

  void _showSharedToast(BuildContext context) {
    ScaffoldMessenger.of(context).showSnackBar(
      const SnackBar(
        content: Text('Document shared successfully'),
      ),
    );
  }

  Future<void> _saveAsFile(
    BuildContext context,
    LayoutCallback build,
    PdfPageFormat pageFormat,
  ) async {
    final bytes = await build(pageFormat);

    final appDocDir = await getApplicationDocumentsDirectory();
    final appDocPath = appDocDir.path;
    final file = File('$appDocPath/document.pdf');
    print('Save as file ${file.path} ...');
    await file.writeAsBytes(bytes);
    await OpenFile.open(file.path);
  }

  
  Widget build(BuildContext context) {
    pw.RichText.debug = true;

    if (_tabController == null) {
      return const Center(child: CircularProgressIndicator());
    }

    final actions = <PdfPreviewAction>[
      if (!kIsWeb)
        PdfPreviewAction(
          icon: const Icon(Icons.save),
          onPressed: _saveAsFile,
        )
    ];

    return Scaffold(
      appBar: AppBar(
        title: const Text('Flutter PDF Demo'),
        bottom: TabBar(
          controller: _tabController,
          tabs: examples.map<Tab>((e) => Tab(text: e.name)).toList(),
          isScrollable: true,
        ),
      ),
      body: PdfPreview(
        maxPageWidth: 700,
        build: (format) => examples[_tab].builder(format, _data),
        actions: actions,
        onPrinted: _showPrintedToast,
        onShared: _showSharedToast,
      ),
      floatingActionButton: FloatingActionButton(
        backgroundColor: Colors.deepOrange,
        onPressed: _showSources,
        child: const Icon(Icons.code),
      ),
    );
  }

  void _showSources() {
    UrlLauncherPlatform.instance.launch(
        'https://github.com/DavBfr/dart_pdf/blob/master/demo/lib/examples/${examples[_tab].file}',
        useSafariVC: false,
        useWebView: true,
        enableJavaScript: false,
        enableDomStorage: false,
        universalLinksOnly: false,
        headers: <String, String>{
          'my_header_key': 'my_header_value',
          'harmony_browser_page': 'pages/LaunchInAppPage'
        });
  }

  Future<String?> askName(BuildContext context) {
    return showDialog<String>(
        barrierDismissible: false,
        context: context,
        builder: (context) {
          final controller = TextEditingController();

          return AlertDialog(
            title: const Text('Please type your name:'),
            contentPadding: const EdgeInsets.symmetric(horizontal: 20),
            content: TextField(
              decoration: const InputDecoration(hintText: '[your name]'),
              controller: controller,
            ),
            actions: [
              TextButton(
                onPressed: () {
                  if (controller.text != '') {
                    Navigator.pop(context, controller.text);
                  }
                },
                child: const Text('OK'),
              ),
            ],
          );
        });
  }
}

运行效果:应用启动后顶部显示 6 个 Tab(RÉSUMÉ / DOCUMENT / INVOICE / REPORT / CALENDAR / CERTIFICATE),切换 Tab 时 PdfPreview 实时渲染对应 PDF 模板。右上角打印图标触发系统打印对话框,分享图标触发系统分享面板,保存按钮将 PDF 写入本地并自动打开。右下角浮动按钮可在浏览器中查看对应模板的源码。

各 PDF 模板(resume.dart / document.dart / invoice.dart / report.dart / calendar.dart / certificate.dart)详见示例工程 demo/lib/examples/ 目录。

五、FAQ

5.1 常见问题

Q1:flutter pub get 解析失败或找不到 printing_ohos 包

报依赖解析错误,或编译报 Target of URI doesn’t exist。最常见是 git 依赖中漏写 path: printing/ohos(适配代码不在仓库根目录):

# pubspec.yaml —— path 不能省略
printing_ohos:
  git:
    url: https://atomgit.com/oh-flutter/fluttertpc_printing.git
    path: printing/ohos    # 必须指定
    ref: master

核对 url / path / ref 三要素齐全;仍失败可将 url 换为社区另一镜像源重试。

Q2:PDF 中的中文显示为方块或空白

pdf 包默认使用 Helvetica 等拉丁字体,不支持中文。必须加载中文 TTF 字体:

// 从 asset 加载中文 TTF 字体
final ttf = await pw.fontFromAssetBundle('assets/fonts/SourceHanSans.ttf');

doc.addPage(
  pw.Page(
    build: (context) => pw.Text(
      '中文内容',
      style: pw.TextStyle(font: ttf, fontSize: 24),
    ),
  ),
);

同时在 pubspec.yaml 的 flutter.assets 中声明字体目录:

flutter:
  assets:
    - assets/fonts/

Q3:调用 layoutPdf 后没有弹出打印对话框

鸿蒙打印服务需要设备连接了打印机(或安装了打印服务应用)。检查设备设置中的"连接与共享 → 打印"是否已启用并添加了打印机。如果仅做 PDF 预览和保存,可先用 _saveAsFile 方式将 PDF 写入本地后查看。

Q4:convertHtml 方法调用报错

鸿蒙适配版不支持 convertHtml 方法,调用会返回错误。这是鸿蒙平台的已知限制,建议使用 pdf 包的 Widget API 直接创建 PDF 文档:

// 不支持
// await Printing.convertHtml(html: '<h1>Hello</h1>');

// 推荐方式:用 pdf 包的 Widget API 创建
final doc = pw.Document();
doc.addPage(
  pw.Page(build: (context) => pw.Text('Hello')),
);

Q5:真机安装失败(HAP 安装报错)

flutter run 构建成功但安装失败,原因是未配置签名。用 DevEco Studio 打开工程的 ohos 目录,依次进入 File > Project Structure > Signing Configs,勾选 Automatically generate signature 后重新运行。

5.2 库本身存在问题:如何提交 Issue

  1. 打开适配仓库 Issues 页面,点击"新建 Issue";
  2. 标题格式:[Bug] 一句话现象,例如 [Bug] layoutPdf 调用后崩溃;
  3. 正文必须包含:复现步骤 / 期望结果 / 实际结果 / 设备与系统版本(鸿蒙设备型号 + 系统版本)/ Flutter 鸿蒙 SDK 版本 / 最小复现代码、日志或截图;
  4. 提交后跟踪仓库维护者回复,修复发布后关注对应 Tag 更新依赖版本。

5.3 能自己解决:如何提交 PR

  1. Fork 适配仓库到个人 AtomGit 账号;
  2. git clone 自己的 fork,基于 master 新建分支:git checkout -b fix/xxx;
  3. 修改代码(如 ohos 侧 ArkTS 实现、接口层 Dart 代码)并 commit,commit message 说明修改点;
  4. push 到自己的 fork,在原仓库发起 Pull Request(源分支 = 你的修复分支,目标分支 = master);
  5. PR 描述写清:问题背景 / 修改点 / 鸿蒙真机验证结果(附运行截图),等待维护者评审合入。

六、其他内容

6.1 总结

printing 鸿蒙适配版以较低的接入成本,为 Flutter 鸿蒙应用补齐了 PDF 文档生成、打印、预览和分享能力:一个 Printing 静态类即可完成 PDF 打印、分享、光栅化等全部功能,配合 PdfPreview 组件可实现应用内实时预览。该库 API 覆盖完整,支持多种 PDF 模板创建、字体加载、图片嵌入等核心场景;引入时注意 path: printing/ohos 与依赖冲突两个关键点即可快速跑通。鸿蒙侧原生实现调用 PDFKit 解析 PDF、系统打印服务完成打印输出、ShareKit 实现文件分享,convertHtml 方法暂不支持,建议使用 pdf 包的 Widget API 直接创建文档。建议生产环境用 tag 或 commit 锁定依赖版本,遇到问题优先查看适配仓库 Issues。

6.2 参考链接

欢迎加入 CPF-Flutter 鸿蒙社区,社区入口、环境搭建指南和三方库链接统一放在这里:

Logo

讨论HarmonyOS开发技术,专注于API与组件、DevEco Studio、测试、元服务和应用上架分发等。

更多推荐