Flutter 鸿蒙适配版实战:printing PDF 打印插件在 HarmonyOS 上的接入与使用
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 文档生成、打印、预览和分享能力。具体包括:
- PDF 文档生成(配合 pdf 包创建文字、图片、表格、图表等丰富内容);
- 系统打印(调用鸿蒙打印服务,弹出打印对话框选择打印机);
- PDF 预览(PdfPreview 组件在应用内实时预览 PDF);
- PDF 分享(通过系统分享面板将 PDF 发送给其他应用);
- PDF 转图片(将 PDF 页面渲染为位图,用于缩略图或截图);
- 保存为文件(将 PDF 写入本地文件系统并用其他应用打开)。
三、功能介绍
| 功能 | 说明 | 适用场景 |
|---|---|---|
| 打印 PDF | layoutPdf() 调用系统打印对话框输出 PDF | 办公打印、发票打印、报表打印 |
| 列出打印机 | listPrinters() 枚举系统可用打印机 | 指定打印机直连打印 |
| 选择打印机 | pickPrinter() 弹出打印机选择对话框 | 用户交互式选择打印目标 |
| 直接打印 | directPrintPdf() 跳过 UI 直接向指定打印机发送 | 自动化打印、固定打印机场景 |
| 分享 PDF | sharePdf() 调用系统分享面板发送 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
- 打开适配仓库 Issues 页面,点击"新建 Issue";
- 标题格式:[Bug] 一句话现象,例如 [Bug] layoutPdf 调用后崩溃;
- 正文必须包含:复现步骤 / 期望结果 / 实际结果 / 设备与系统版本(鸿蒙设备型号 + 系统版本)/ Flutter 鸿蒙 SDK 版本 / 最小复现代码、日志或截图;
- 提交后跟踪仓库维护者回复,修复发布后关注对应 Tag 更新依赖版本。
5.3 能自己解决:如何提交 PR
- Fork 适配仓库到个人 AtomGit 账号;
- git clone 自己的 fork,基于 master 新建分支:git checkout -b fix/xxx;
- 修改代码(如 ohos 侧 ArkTS 实现、接口层 Dart 代码)并 commit,commit message 说明修改点;
- push 到自己的 fork,在原仓库发起 Pull Request(源分支 = 你的修复分支,目标分支 = master);
- 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 鸿蒙社区,社区入口、环境搭建指南和三方库链接统一放在这里:
更多推荐

所有评论(0)