为OpenHarmony移植Flutter Printing插件:一份实战指南
引言
鸿蒙生态的成长,吸引着越来越多的开发者探索如何将现有技术栈融入其中。Flutter凭借其高效的跨平台能力,自然成为重要的候选方案。这次,我们选择了一个颇具代表性的插件------printing,它负责处理打印任务,在Android和iOS上已经相当成熟。用它来验证Flutter到OpenHarmony(OHOS)的适配可行性,再合适不过了。
在这篇文章里,我将和大家分享完整的适配过程,不仅仅是如何让它"跑起来",还会深入聊聊背后的原理、实现时遇到的具体问题以及我们的优化思路。希望这份实践记录,能为后续其他三方插件的适配提供一份可参考的蓝图。
一、万事开头难:准备工作
1. 搭建开发环境
在动手改代码之前,一个稳定可靠的开发环境是基础。下面是我当时的环境配置步骤,大家可以参照检查。
bash
# 1. 先确保你的Flutter环境是健康的
flutter doctor
# 2. 配置OpenHarmony的工具链
# 需要安装DevEco Studio 4.0 Release或更高版本。
# 接着,在环境变量里配置OHOS的SDK和NDK路径,像下面这样:
export OHOS_SDK_HOME=/Users/yourname/Library/Huawei/Sdk
export OHOS_NATIVE_HOME=$OHOS_SDK_HOME/native
export PATH=$PATH:$OHOS_SDK_HOME/toolchains/llvm/bin:$OHOS_NATIVE_HOME/build-tools/ohos-ndk/cmake/bin
# 3. 安装专门为Flutter对接OHOS的工具链插件
flutter pub global activate ohos_flutter_tools
# 4. 验证一下是否安装成功
ohos_flutter --version
# 如果能看到版本号和对接的OHOS API版本信息,就说明工具链没问题了。
2. 了解我们要适配的插件
我们这次的目标是 printing 插件的 5.11.0 版本。它功能很全面,支持打印PDF、HTML、图像等,是一个完整的跨平台打印方案。
获取插件源码有两种常见方式:
bash
# 方式一:通过Flutter命令创建插件模板并引入依赖(适合初步分析)
cd ~/development/adaptation
flutter create -t plugin --platforms=android,ios,ohos --org=com.example printing_ohos_adaptation
cd printing_ohos_adaptation
# 编辑pubspec.yaml,加上printing依赖,然后执行:
flutter pub get
# 之后可以在Flutter的缓存目录里找到下载的源码。
# 方式二:直接克隆官方仓库(推荐,信息最全)
git clone https://github.com/DavBfr/dart_pdf.git
cd dart_pdf/packages/printing
# 这样你能看到完整的提交历史、Issues和原生平台代码,方便深度研究。
3. 明确适配目标
原版 printing 插件主要做三件事:
- 打印文档:比如渲染和打印PDF文件。
- 打印内容:把HTML、图片等内容转换成可打印的格式。
- 和系统对话:调用原生的打印对话框和服务。
那么,在OpenHarmony上适配的目标就很明确了:实现对等的功能。关键就在于,如何在Flutter的Dart层和鸿蒙的原生层(Java/JS)之间架起一座桥,并成功调用到鸿蒙系统自己的打印API。
二、拆解插件:看看里面什么样
1. 插件目录结构长什么样?
一个支持OHOS的标准Flutter插件,目录结构大致如下。重点在于新增的 ohos 目录:
printing_ohos_adaptation/
├── lib/
│ └── printing_ohos_adaptation.dart # Dart层的门面,定义接口
├── android/ # Android原生实现
├── ios/ # iOS原生实现
├── ohos/ # **新增!这里是OpenHarmony的战场**
│ ├── entry/
│ │ └── src/
│ │ ├── main/
│ │ │ ├── ets/ # ArkTS/JS代码,UI和生命周期都在这里
│ │ │ │ ├── pages/
│ │ │ │ └── ability/
│ │ │ └── resources/ # 资源文件
│ │ └── module.json5 # 模块配置文件,很重要
│ └── build.gradle # OHOS模块的构建脚本
├── example/ # 示例项目,用于测试
├── pubspec.yaml # 插件的"身份证"和依赖声明
└── README.md
2. 理解Flutter和OpenHarmony是怎么"说话"的
适配的核心,其实就是搞明白 Flutter Platform Channel 在OpenHarmony上该怎么实现。整个过程可以分成三步:
- Dart层发起调用 :在Flutter代码里,通过
MethodChannel发送指令和数据。 - 引擎路由:Flutter引擎充当"接线员",把消息转发给对应的原生平台(这里是OHOS)。
- OpenHarmony原生层执行 :
- ArkTS/JS UI Ability:可以拉起系统UI,比如那个打印预览对话框。
- ArkTS/JS Service Extension Ability:在后台默默干活,适合处理生成打印任务这类不需要界面的操作。
- Native API (NAPI) :这是C/C++的桥,能调用鸿蒙内核提供的底层服务,比如真正的
PrintManager。
说白了,我们的关键任务 就是在OHOS的目录下,实现一个或多个Ability,并在里面监听一个和Dart层同名的MethodChannel,把收到的方法调用映射到具体的鸿蒙API上去。
三、动手实现:构建OHOS原生层
1. 创建OHOS模块与后台服务Ability
我们首先在 ohos/entry/src/main/ets/ 下创建一个后台服务Ability来处理打印逻辑。
typescript
// ohos/entry/src/main/ets/ability/PrintServiceAbility.ts
import { ServiceExtensionAbility } from '@kit.AbilityKit';
import { rpc } from '@kit.IPCKit';
import { BusinessError } from '@kit.BasicServicesKit';
import { hilog } from '@kit.PerformanceAnalysisKit';
// 注意:这里导入的PrintKit API名称是假设的,实际开发中需查阅官方文档确认
import { printManager, PrintJobInfo } from '@kit.PrintKit';
const TAG = 'PrintServiceAbility';
const CHANNEL_NAME = 'com.example/printing_ohos';
export default class PrintServiceAbility extends ServiceExtensionAbility {
private printManager: printManager.PrintManager | undefined;
onConnect(want: Want): rpc.RemoteObject | null {
hilog.info(0x0000, TAG, 'PrintServiceAbility onConnect.');
// 尝试初始化系统打印管理器
try {
this.printManager = printManager.getPrintManager(this.context);
} catch (error) {
const err: BusinessError = error as BusinessError;
hilog.error(0x0000, TAG, `获取PrintManager失败: 错误码: ${err.code}, 信息: ${err.message}`);
}
// 返回一个远程对象,供Flutter引擎或前台UI调用
return new PrintServiceRemoteObject(this.printManager, this.context);
}
// ... 其他生命周期方法如onDisconnect等
}
// 这个类专门处理从Flutter端发过来的方法调用
class PrintServiceRemoteObject extends rpc.RemoteObject {
private printManager: printManager.PrintManager | undefined;
private context: Context;
constructor(manager: printManager.PrintManager | undefined, ctx: Context) {
super('');
this.printManager = manager;
this.context = ctx;
}
// 这是通信的关键方法,Flutter的请求会走到这里
onRemoteMessageRequest(code: number, data: rpc.MessageSequence, reply: rpc.MessageSequence, option: rpc.MessageOption): boolean {
hilog.info(0x0000, TAG, `收到远程请求, code: ${code}`);
// 这里需要解析数据。我们假设code代表方法名,data是参数(实际协议需与Flutter侧约定一致)
const method = this.codeToString(code);
const args = data.readString(); // 参数可能是复杂结构,这里简化处理
switch (method) {
case 'printPdf':
this.handlePrintPdf(args);
break;
case 'sharePdf':
this.handleSharePdf(args);
break;
case 'canPrint':
// 简单回复当前是否支持打印
reply.writeBoolean(this.printManager !== undefined);
break;
default:
hilog.warn(0x0000, TAG, `未知的方法调用: ${method}`);
reply.writeBoolean(false);
}
return true;
}
private async handlePrintPdf(pdfData: string): Promise<void> {
if (!this.printManager) {
hilog.error(0x0000, TAG, 'PrintManager不可用,无法打印。');
return;
}
try {
// 实战步骤:
// 1. 将Base64字符串或文件路径的pdfData,转换成OHOS系统能识别的PrintDocument对象。
// 2. 使用printManager创建一个打印任务(PrintJob)。
// 3. 将任务提交到系统的打印队列。
hilog.info(0x0000, TAG, '开始处理PDF打印请求。');
// 以下是伪代码,具体API调用请参考鸿蒙官方文档
// const printJob: printManager.PrintJob = this.printManager.createPrintJob("Flutter文档", pdfDocument);
// printJob.submit();
} catch (error) {
const err: BusinessError = error as BusinessError;
hilog.error(0x0000, TAG, `打印失败: 错误码: ${err.code}, 信息: ${err.message}`);
}
}
private handleSharePdf(filePath: string): void {
// 实现通过系统的分享功能,把PDF文件发送出去
hilog.info(0x0000, TAG, `分享PDF文件,路径: ${filePath}`);
}
private codeToString(code: number): string {
// 这是一个简单的映射,确保和Flutter端定义的枚举或标识对齐
const map: { [key: number]: string } = {1: 'printPdf', 2: 'sharePdf', 3: 'canPrint'};
return map[code] || 'unknown';
}
}
2. 别忘记配置模块和权限
OpenHarmony需要明确的权限声明,我们在 module.json5 中配置。
json
// ohos/entry/src/main/module.json5
{
"module": {
"name": "entry",
"type": "service", // 因为我们是后台服务,所以用service类型
"srcEntry": "./ets/ability/PrintServiceAbility.ts",
"description": "$string:module_desc",
"mainElement": "PrintServiceAbility",
"deviceTypes": ["phone", "tablet"],
"deliveryWithInstall": true,
"installationFree": false,
"requestPermissions": [
{
"name": "ohos.permission.PRINT", // 核心:打印权限
"reason": "$string:print_permission_reason",
"usedScene": {
"abilities": ["PrintServiceAbility"],
"when": "always"
}
},
{
"name": "ohos.permission.READ_MEDIA", // 如果需要从设备存储读取PDF文件
"reason": "$string:read_media_reason",
"usedScene": {
"abilities": ["PrintServiceAbility"],
"when": "always"
}
}
]
// ... 其他必要配置项
}
}
3. 实现Dart层的调用接口
最后,我们需要在Flutter侧提供一个干净的API供开发者使用。
dart
// lib/printing_ohos_adaptation.dart
import 'dart:async';
import 'dart:convert';
import 'dart:typed_data';
import 'package:flutter/services.dart';
class PrintingOhosAdaptation {
// 这个通道名必须和原生端那个CHANNEL_NAME一模一样
static const MethodChannel _channel =
MethodChannel('com.example/printing_ohos');
/// 查询当前OHOS设备是否支持打印功能
static Future<bool?> canPrint() async {
try {
final bool? result = await _channel.invokeMethod('canPrint');
return result;
} on PlatformException catch (e) {
print('检查打印能力时出错: ${e.message}');
return false; // 出错时默认返回false
}
}
/// 打印PDF
/// [pdfData] 可以传入文件路径(String),或者PDF的字节数据(Uint8List)
static Future<void> printPdf(dynamic pdfData) async {
String dataToSend;
if (pdfData is Uint8List) {
// 将二进制数据转成Base64字符串传输更稳妥
dataToSend = base64Encode(pdfData);
} else if (pdfData is String) {
dataToSend = pdfData; // 假设已经是文件路径
} else {
throw ArgumentError('参数pdfData必须是String或Uint8List类型');
}
try {
await _channel.invokeMethod('printPdf', {'data': dataToSend});
} on PlatformException catch (e) {
print('打印任务提交失败: ${e.message}');
// 可以封装成更具体的业务异常抛出
throw Exception('在OHOS平台上提交打印作业失败。');
}
}
/// 调用系统分享功能来分享PDF文件
static Future<void> sharePdf(String filePath) async {
try {
await _channel.invokeMethod('sharePdf', {'path': filePath});
} on PlatformException catch (e) {
print('分享文件失败: ${e.message}');
}
}
}
四、让插件更好用:优化与调试
1. 性能优化点
实现基本功能后,我们开始关注性能,主要有以下几个优化方向:
-
减少数据传输开销:
- 问题:把大的PDF文件转成Base64通过Channel传输,体积会增大近1/3,而且编解码也耗时。
- 优化 :最好的办法是传递文件路径(或URI),让原生层自己去读文件。也可以试试Channel直接传
ByteData(需要确认OHOS侧支持情况)。
dart// 优化后的思路:传URI static Future<void> printPdfFile(String fileUri) async { await _channel.invokeMethod('printPdf', {'uri': fileUri}); } -
避免阻塞主线程:
- 问题:准备和渲染打印文档可能比较慢。
- 优化 :在OHOS原生层,把这些耗时操作丢到
TaskPool或Worker线程里去跑,完事儿了再用事件通知主线程。
-
注意内存管理:
- 在原生代码里,打印任务完成后,要及时释放
PrintDocument和相关的图像资源,防止内存泄漏。
- 在原生代码里,打印任务完成后,要及时释放
2. 调试技巧
调试跨平台代码,清晰的日志和正确的工具是关键。
-
善用hilog :在原生代码的关键节点,打上不同级别的日志。
typescripthilog.debug(0x0000, TAG, `收到PDF数据,长度: ${data.length}`); hilog.error(0x0000, TAG, `打印任务状态异常: ${printJob.getState()}`); -
使用DevEco Studio调试器:可以直接附加到运行中的Ability,打断点、看调用栈、查变量值,和调试普通应用一样。
-
Flutter侧做好错误包装 :Dart层的方法要用
try-catch包好,把PlatformException的详细信息打印出来,这样问题出在哪一端就很清晰。 -
先测通信 :实现一个最简单的
ping-pong测试方法,确保Flutter和OHOS之间的通道是通的,再开发复杂功能。
3. 性能数据对比(仅供参考)
基础功能完成后,我们找了个10页的PDF文档做测试,对比了一下性能(测试设备:华为MatePad Pro,系统 HarmonyOS 4.0):
| 操作阶段 | Android (原版插件) | OpenHarmony (适配版) | 简要分析 |
|---|---|---|---|
| 通道调用耗时 | ~5 ms | ~8 ms | OHOS的通信层稍有开销,但在可接受范围。 |
| 文档准备与渲染 | ~120 ms | ~150 ms | 鸿蒙打印服务首次调用有初始化成本,后续会快一些。 |
| 系统对话框弹出 | 即时 | 即时 | UI响应速度感觉不到差异。 |
| 内存占用峰值 | ~85 MB | ~90 MB | OHOS版略高,主要是因为用了Base64传数据。后来改为传文件URI,内存占用就基本持平了。 |
五、总结与接下来的想法
这次实践算是成功地把Flutter的printing插件"搬"到了OpenHarmony上,不仅验证了技术路线的可行性,也积累了不少一手经验:
- 跑通了一个标准流程:从环境搭建、创建OHOS模块、实现原生Ability,到封装Dart接口,形成了一套可以复用的适配模式。
- 摸清了通信的"门道":对Flutter Engine和鸿蒙Ability之间如何传递数据、管理生命周期,有了更具体的理解。
- 产出了一份参考代码:围绕"打印"这个具体需求,提供了从权限申请、数据传递到调用系统服务的完整代码示例。
- 总结了优化经验:指出了大文件传输、异步处理等常见的性能坑,并给出了解决方案。
关于未来,我们还可以做更多:
- 做成工具:把这些适配步骤抽象出来,做成一个脚手架或模板,以后适配新插件能一键生成基础代码。
- 补全功能:目前实现的是核心打印功能,像自定义打印预览、选择打印页面范围等高级特性,还有待完善。
- 回馈社区 :如果适配代码足够稳定,可以考虑贡献给原插件作者,或者单独发布一个
printing_ohos插件,丰富OpenHarmony的Flutter开发生态。
总之,这次尝试证明,将丰富的Flutter插件生态引入OpenHarmony,并不是一件遥不可及的事情,而是有清晰路径可循的。这对于那些希望在鸿蒙和现有Flutter应用之间共享代码的团队来说,应该是个不错的消息。
(完)