☰
Flutter for OpenHarmony 实战:三方库 large_file_handler 的鸿蒙化适配指南
2026/9/27 2:36:33 网站建设 项目流程

环境搭建指引:https://atomgit.com/CPF-Flutter/flutter_samples/blob/master/docs/ohos/getting-started/flutter-oh-env-setup.md

large_file_handler解决的是一个很具体的问题:把大文件(Flutter 资源或远程 URL)复制到应用的本地目录,并且边复制边报进度。它的 0.5.2 版支持 Android、iOS、macOS(原生实现)和 Windows、Linux(纯 Dart 实现),没有 OpenHarmony。

这个库的接口少得可爱——五个方法、一条事件通道——但它在鸿蒙上踩到的那个坑很值得写下来:分块读写时偏移量算错,文件大小会分毫不差、内容却整段整段地错。这种错误用"文件存在、大小对"是查不出来的,本篇因此专门做了一套逐字节比对,并且真的靠它抓到了一个 bug。

适配对象:上游large_file_handler0.5.2(MIT,对应 main7987f70);适配产物 TAG0.5.2-ohos-1.0.0-beta.1。


一、这个库要解决什么

1.1 上游 API

finalhandler=LargeFileHandler();// 资源 -> 本地awaithandler.copyAssetToLocalStorage(assetName:'x.bin',targetPath:'x.bin');Stream<int>p1=handler.copyAssetToLocalStorageWithProgress(assetName:'x.bin',targetPath:'x.bin');// URL -> 本地awaithandler.copyNetworkAssetToLocalStorage(assetUrl:url,targetPath:'y.bin');Stream<int>p2=handler.copyNetworkAssetToLocalStorageWithProgress(assetUrl:url,targetPath:'y.bin');// 是否存在bool exists=awaithandler.fileExists(targetPath:'x.bin');

三个细节决定了适配的形状:

  1. targetPath是相对的,由插件自己用path_provider的getApplicationDocumentsDirectory()拼成绝对路径再交给原生;
  2. 带进度与不带进度是四个独立的方法(不是"传个回调"),它们共用同一条事件通道file_download_progress;
  3. 进度值就是0-100 的整数,复制结束时原生侧还会endOfStream()收尾。

1.2 契约

finalmethodChannel=constMethodChannel('large_file_handler');finalprogressChannel=constEventChannel('file_download_progress');// 五个方法,参数都是 {assetName, targetPath} 或 {url, targetPath} 或 {targetPath}// copyAssetToLocal / copyAssetToLocalWithProgress// copyUrlToLocal / copyUrlToLocalWithProgress// fileExists -> bool

pubspec 里的平台声明是这样的:

android:{pluginClass:LargeFileHandlerPlugin}ios:{pluginClass:LargeFileHandlerPlugin}macos:{pluginClass:LargeFileHandlerPlugin}windows:{dartPluginClass:LargeFileHandlerDesktop}# 纯 Dartlinux:{dartPluginClass:LargeFileHandlerDesktop}# 纯 Dartweb:{pluginClass:LargeFileHandlerWeb}

而且lib/src/default_instance_io.dart里写得很清楚:

LargeFileHandlerPlatform?buildDefaultInstance()=>MethodChannelLargeFileHandler();

没有平台门、没有defaultTargetPlatform分支——鸿蒙上平台接口的默认实现就是这条方法通道的实现,原生侧把通道接住即可。

1.3 一个不能忘的前置依赖

插件 Dart 侧用path_provider把相对路径解析成绝对路径,而path_provider自己没有为 ohos 声明default_package:

# path_provider 2.1.6 的 pubspecplugin:platforms:android:{default_package:path_provider_android}ios:{default_package:path_provider_foundation}linux:{default_package:path_provider_linux}macos:{default_package:path_provider_foundation}windows:{default_package:path_provider_windows}

也就是说:pub get不会自动带上鸿蒙实现。鸿蒙宿主必须自己加一行:

dependencies:path_provider_ohos:^2.2.1

少了它,插件里第一个方法调用就是MissingPluginException。示例工程加上之后,getApplicationDocumentsDirectory()在鸿蒙上返回<应用 filesDir>/flutter:

应用文档目录 /data/storage/el2/base/files/flutter

二、选库:四道筛 + 在线查重

2.1 四筛

筛结果
① pub.dev 平台列表有没有ohosandroid, ios, macos, windows, linux, web—— 没有,通过
② 上游根目录 / pub.dev 有没有*_ohos兄弟包根目录无;large_file_handler_ohos返回 404,通过
③ Dart 入口有没有平台门只有MethodChannel/EventChannel,没有Platform.is*、defaultTargetPlatform、UnsupportedError,通过
④ 依赖体检node .agents/tools/dep-ohos-check.mjs large_file_handler→ 依赖只有path_provider(有path_provider_ohos)与纯 Dart 包,通过

第四道筛是上一轮新增的:一个插件能不能在鸿蒙上跑,还取决于它依赖的原生插件有没有鸿蒙实现。这一条对large_file_handler的结论不是"通过就完事",而是直接产出了 1.3 节的宿主要求——依赖有实现,但必须显式引入。

2.2 在线查重

$ node .agents/tools/live-dedup.mjs large_file_handler pub.dev : v0.5.2 platforms=[android,ios,macos,windows,linux,web] atomgit : 四个组织(oh-flutter / CPF-Flutter / hxa-flutter / oh-tpc)均无同名仓库

顺带确认没有撞同主题的库:oh-flutter/flutter_downloader_ohos、CPF-Flutter/fluttertpc_flutter_downloader、fluttertpc_flutter_file_downloader都是另一个库(flutter_downloader/flutter_file_downloader,偏"下载任务管理"),而large_file_handler的定位是"把一份数据可靠地写到指定路径并报进度",接口只有五个方法。

2.3 基线

上游仓库没有发布任何 tag,所以基线取main的7987f70,并逐文件核对它与 pub.dev 上 0.5.2 的归档一致:

identical after EOL normalisation: 8 / 8 still different: []

(上一轮踩过"git tag 落后于已发布版本"的坑,这一步现在每篇都做。)


三、六步适配流程

第一步:把上游同步到 AtomGit

node.agents\tools\atomgit.mjs create oh-flutter large_file_handler"<中文描述>"node.agents\tools\atomgit.mjs verify oh-flutter large_file_handler# 回读校验中文没被 ASCII 毁掉

第二步:本地克隆(并用发布版核对基线)

git clone https://gh-proxy.com/https://github.com/DenisovAV/large_file_handler.git lfh_work cd lfh_work git log--oneline-1# 7987f70 Merge pull request #12 ...git tag# (空:上游没有 tag)

第三步:建分支并用框架命令补出鸿蒙化目录

git checkout-b feat/ohos_large_file_handler_0.5.2 flutter create-t plugin--platforms ohos--org com.example.

这一次.metadata里本来就是project_type: plugin,省掉了上一轮那两个报错(-t plugin与project_type不匹配、--org必填)。但模板垃圾依旧,见第六节。

第四步:适配过程(新增了什么、为什么)

文件作用
ohos/index.etsHAR 出口
ohos/src/main/ets/components/plugin/LargeFileHandlerPlugin.ets插件本体:五个方法 + 事件通道
pubspec.yaml新增ohos: pluginClass: LargeFileHandlerPlugin

第五步:补全额外文件

两份 README(中英)+ CHANGELOG,并在根README.md的 Supported Platforms 表里加一行 OpenHarmony。

第六步:推送并打 TAG

git add-A && git commit-F commit-msg.txt git remote add atomgit https://atomgit.com/oh-flutter/large_file_handler.git git push atomgit HEAD:main git push atomgit feat/ohos_large_file_handler_0.5.2 git tag-a 0.5.2-ohos-1.0.0-beta.1-m"large_file_handler OpenHarmony 适配 0.5.2-ohos-1.0.0-beta.1"git push atomgit 0.5.2-ohos-1.0.0-beta.1

推完的仓库首页:


四、代码写在哪个文件

large_file_handler/ ├── ohos/ │ ├── index.ets # HAR 出口(flutter create 生成) │ ├── oh-package.json5 # 包名/版本/协议(生成后手改) │ └── src/main/ets/components/plugin/ │ └── LargeFileHandlerPlugin.ets # ★ 插件本体 ├── example/ │ ├── assets/sample_1mb.bin # ★ 1MiB 随机资源,用来验证"逐字节一致" │ ├── lib/main.dart # ★ 鸿蒙演示页(本次重写) │ └── ohos/ # 应用工程(flutter create 生成) └── pubspec.yaml # ★ 新增 ohos: pluginClass

4.1 资源复制:getRawFd()+ 分块读写

Flutter 资源在 HAP 里的落点是固定的,这一点可以直接从构建产物里确认:

$ tar -tf entry-default-signed.hap | findstr sample_1mb resources/rawfile/flutter_assets/assets/sample_1mb.bin

所以 rawfile 路径 =flutter_assets/+ Dart 传来的assets/<名字>。拿到 fd 之后分块处理:

constraw=awaitcontext.resourceManager.getRawFd(rawPath);// { fd, offset, length }constdest=fs.openSync(targetPath,fs.OpenMode.READ_WRITE|fs.OpenMode.CREATE|fs.OpenMode.TRUNC);constbuffer=newArrayBuffer(CHUNK_SIZE);letread=0;for(;;){constwant=Math.min(CHUNK_SIZE,raw.length-read);if(want<=0)break;// 关键:每次都要给绝对偏移constlength=fs.readSync(raw.fd,buffer,{offset:raw.offset+read,length:want});if(length<=0)break;fs.writeSync(dest.fd,buffer,{length:length});read+=length;if(withProgress)this.reportBytes(read,raw.length);}

4.2 URL 下载:http.requestInStream()流式写盘

constrequest=http.createHttp();request.on('dataReceiveProgress',(info)=>{total=info.totalSize;});request.on('dataReceive',(chunk)=>{fs.writeSync(dest.fd,chunk,{length:chunk.byteLength});written+=chunk.byteLength;if(withProgress&&total>0)this.reportBytes(written,total);});constcode=awaitrequest.requestInStream(url,{method:http.RequestMethod.GET,connectTimeout:30000,readTimeout:0,// 大文件不设读超时expectDataType:http.HttpDataType.ARRAY_BUFFER,});if(code!==200&&code!==206)thrownewError(`Failed to download file:${code}`);

为什么不用@ohos.request(系统下载代理):它把任务交给系统服务、产物落进系统下载目录、还附带系统通知,与本库"写到你给的targetPath、进度由这条通道上报"的契约不一致;另外模拟器上根本没有对应的系统能力(hidumper -ls列出的 98 个服务里没有 request/download 相关项)。用进程内的流式 HTTP,一来契约完全吻合,二来几十 MB 的响应体也不会整块进内存。

4.3 进度:0-100整数 + 收尾endOfStream

上游 Android 每读 1KB 报一次、结束补 100 并endOfStream(),Dart 侧靠onDone收尾。鸿蒙侧沿用同样的可观测语义,只做两处工程化处理:

  • 块大小取64KB(1KB 对几十 MB 的文件会产生几万条事件);
  • 只在百分比变化时上报(同一百分比不重复发)。

失败时把错误推进进度流:sink.error('ERROR', message, null)。上游 0.5.1 专门修过这一点——否则调用方会看到"进度流正常结束",以为文件复制成功了。


五、一个只有逐字节比对才能抓住的坑

第一版的资源复制是这样写的:“第一块按raw.offset读,之后的块交给文件指针自己前进”:

// ❌ 第一版constlength=first?fs.readSync(raw.fd,buffer,{offset:raw.offset,length:want}):fs.readSync(raw.fd,buffer,{length:want});

结果是:第二次起每次都从头读,写出来的文件是"第一块 64KB 重复 16 遍"。而它的表现是:

检查项结果
文件是否存在✅ 存在
文件大小✅ 1048576 字节(与资源一模一样)
进度事件✅ 16 个(6, 12, 18, 25, 31, 37 … 100)
内容❌ 全错——每块都是第一块

一开始我用 SHA-256 看出了差异,但直到在演示页里加了一个"与 asset 逐字节比对"的按钮,才把问题定位到具体字节:

资源复制结果逐字节比对 第 65536 字节起不同:asset=0xd6,文件=0x50

65536 正好是块大小,指向"从第二块开始读错"。改成每次都传绝对偏移(offset: raw.offset + read)之后:

资源复制结果逐字节比对 逐字节一致(1048576 字节)

这条经验值得单独记一笔:验证"文件复制"时,"大小对不对"几乎等于没验证。分块读写里的偏移、缓冲区复用、最后一块的边界,任何一个出错都不会改变文件大小。要么对哈希,要么逐字节比。


六、编译与构建踩坑

6.1integration_test会让鸿蒙构建直接失败

上游示例的dev_dependencies里有integration_test: { sdk: flutter },鸿蒙构建时:

> hvigor ERROR: AdaptorError 00303231 Configuration Error Error Message: The srcPath is not a relative path: D:/flutter/flutter_flutter/packages/integration_test/ohos

工具会把每个"dev 依赖里的插件"都登记给 OHOS,而integration_test这个 SDK 包没有 ohos 模块,于是 hvigor 拿到一个绝对路径就报错。处理办法是从示例里移除该依赖,并说明上游那两个集成测试为什么没有保留——这属于示例侧的取舍,插件本身不受影响。

6.2 ArkTS 不允许抛"联合类型"

copyUrl里我原本把写盘失败先存成let writeError: Error | null,请求结束后再throw writeError,ArkTS 编译器不认:

Error Message: "throw" statements cannot accept values of arbitrary types (arkts-limited-throw)

改成存字符串消息、需要时throw new Error(...)即可:

letwriteErrorMessage:string='';// ...if(writeErrorMessage!=='')thrownewError(writeErrorMessage);

6.3 模板垃圾这次进了lib/、test/,还多出linux/、windows/

flutter create -t plugin --platforms ohos .之后,git ls-files --others --exclude-standard列出了 23 项不该存在的东西:

类别例子处理
平台模板android/*.gradle.kts、linux/、windows/删(本插件的 Linux/Windows 是纯 Dart 实现,本来就没有原生目录)
Dart 模板lib/large_file_handler_method_channel.dart、test/large_file_handler_test.dart删(会污染包的公开 API)
示例模板example/android/*.kts、example/ios/.../SceneDelegate.swift、example/test/删
隐私清单ios/.../PrivacyInfo.xcprivacy、macos/.../PrivacyInfo.xcprivacy删

清理姿势(与上一轮一致,重点是只删未跟踪的文件):

$untracked= gitls-files--others--exclude-standard$junk=$untracked|Where-Object{$_-notmatch'(^|/)ohos/'}foreach($fin$junk){Remove-Item-LiteralPath$f-Force}

6.4 其它

  • flutter pub get会把别的平台生成的注册文件按 CRLF 重写,git status里出现一堆"已修改"。用git diff --ignore-cr-at-eol --quiet判断:只是换行符差异就git checkout --掉,别把噪音带进提交。
  • release 构建下Log.i不输出(引擎Log类默认级别 WARN),验证一律flutter build hap --debug --target-platform ohos-x64。
  • ABI /PUB_CACHE/ 唤醒设备 / 签名:--target-platform ohos-x64、$env:PUB_CACHE="E:\pub-cache"、hdc shell power-shell wakeup、先在example/ohos跑devecocli signature generate,提交前清空signingConfigs。

七、真机(模拟器)验证

7.1 验证环境与数据源

项值
Flutter for OpenHarmony SDK3.44.9+ohos-0.0.1-canary1(Dart 3.12.2)
DevEco Studio26.0.0.621(OpenHarmony SDK API 26)
设备Pura X View 模拟器,HarmonyOS 7.0.0(26.0.0) Beta2,ohos-x64
下载源宿主机器上的 HTTP 服务(模拟器经 QEMU NAT 访问10.0.2.2),40MiB 随机数据

下载源刻意放在宿主机器上而不是公网:http-test-server.mjs支持限速,本次设成 2.5MB/s,40MiB 需要约 17 秒,足够让进度条在截图里停在中间、也足够让进度事件跑满 101 个点。

node.agents/tools/http-test-server.mjs httpd--port8899--throttle2500000# httpd/test_40mb.bin: 41943040 字节,sha256 = 6be2cf5ea35faaaf388faecc8d55b4935be4e00488d6cc3f0e387165178a76aa

7.2 实测数据

操作文件大小应用内 SHA-256(前 16 位)期望值(宿主侧)
复制资源(带进度)sample_1mb.bin104857686a786ac32eb8523…86a786ac32eb8523…(HAP 内资源)
下载 URL(带进度)download_40mb.bin419430406be2cf5ea35faaaf…6be2cf5ea35faaaf…(宿主文件)

配合界面上的进度统计:

操作进度点数量序列
复制资源(带进度)166, 12, 18, 25, 31, 37 … 100(1MiB ÷ 64KB = 16 块)
下载 URL(带进度)1010, 1, 2, 3, 4, 5 … 100(每个百分比一个点)
复制资源(不带进度)0事件通道没有被订阅(日志里没有progress stream listened)
下载 URL(不带进度)0同上

原生日志(节选):

LargeFileHandlerPlugin --> large_file_handler channels registered LargeFileHandlerPlugin --> progress stream listened LargeFileHandlerPlugin --> copied asset assets/sample_1mb.bin -> /data/storage/el2/base/files/flutter/sample_1mb.bin (1048576 bytes) LargeFileHandlerPlugin --> progress stream cancelled LargeFileHandlerPlugin --> downloaded http://10.0.2.2:8899/test_40mb.bin -> /data/storage/el2/base/files/flutter/download_40mb.bin (41943040 bytes, http 200)

启动后的样子(两个目标文件都还不存在):

复制完成后:进度点 16 个、SHA-256 与 HAP 内资源一致、逐字节比对通过。

下载进行到 33% 时的界面(进度流已记录 34 个点,说明是连续上报而不是只在结束时给一个 100):

下载完成后的最终状态:


八、已知限制

  1. 资源复制依赖getRawFd():资源必须随 HAP 打包(Flutter 资源的默认落点就是resources/rawfile/flutter_assets/)。
  2. URL 下载在应用进程内完成:不申请后台任务权限,也不做断点续传(本库的 Dart 接口没有这两个语义);应用退到后台后能否继续下载取决于系统策略。
  3. 进度事件数量与上游不同:块大小 1KB → 64KB、相同百分比去重,可观测的序列一致但事件更少。
  4. 示例移除了integration_test:见 6.1,这是示例侧取舍。
  5. 示例里多了crypto依赖:只用于演示页在设备上算 SHA-256(纯 Dart)。
  6. 示例资源是 1MiB 随机数据:为了让"逐字节比对"和"进度 16 个点"可复现;仓库因此多了 1MiB 二进制文件。

九、常见问题

Q1:为什么鸿蒙宿主还要额外加path_provider_ohos?

因为本插件 Dart 侧调用path_provider的getApplicationDocumentsDirectory()来解析相对路径,而path_provider只为 android/ios/linux/macos/windows 声明了default_package,没有为 ohos 背书,pub get不会自动把path_provider_ohos拉进来。这是鸿蒙生态里用官方插件的一个通例:主包给你 API,<包名>_ohos给你实现,缺一不可。加上之后实测返回/data/storage/el2/base/files/flutter。

Q2:为什么不直接用@ohos.request的下载代理?它不是更"系统"吗?

它确实是系统能力,但语义与本库不匹配:任务交给系统服务、文件落到系统下载目录、还弹系统通知,而本库要求"写到我给的targetPath、进度走我这条事件通道"。另外模拟器上没有该项系统能力(hidumper -ls的 98 个服务里没有 request/download 相关项),走它会让演示完全无法验证。用http.requestInStream()既符合契约,又不需要任何系统服务。

Q3:分块读为什么非要每次给绝对偏移?

resourceManager.getRawFd()返回的 fd 指向整个 HAP 文件,raw.offset才是资源在里面的起点,fs.readSync(fd, buffer, { offset })的 offset 是相对这个 fd 的绝对位置。第一版只给第一块传偏移、指望文件指针自动前进,实际是每次从头读——文件大小依旧正确,内容全错。这个坑在第五节有完整的证据截图。

Q4:怎么证明"复制真的写对了"?

两条:①应用内用crypto算 SHA-256,与源文件(HAP 内资源 / 宿主上的原文件)比对;②再加一个"与 asset 逐字节比对"按钮,报出第一个不同字节的位置。只看"文件存在 + 大小相等"是不够的——本节的 bug 就是这样漏掉的。为了让比对有意义,示例资源用的是 1MiB 随机数据(不是可压缩的文本)。

Q5:进度为什么不是 1KB 报一次?会不会和上游行为不一致?

值域与收尾语义完全一致(0-100 整数、结束补 100 并endOfStream()),只是把块从 1KB 放大到 64KB 并对重复百分比去重。对调用方来说,Stream<int>的可观测内容一样(都是从低到高、以 100 结束),只是事件更少。40MiB 的下载实测是 101 个点(每个百分比一个)。

Q6:下载失败会怎样?

原生侧先把错误写进进度流(sink.error('ERROR', message, null)),再让方法调用以PlatformException('DOWNLOAD_FAILED', ...)失败——与上游 0.5.1 之后的约定一致。Dart 侧_progressStream会把事件通道的错误导入调用方正在监听的那条流,所以调用方不会看到"流正常结束"这种假成功。非 2xx 响应会被当作失败并带上 HTTP 状态码。

Q7:不带进度的两个方法会不会也订阅事件通道?

不会。实测日志里只有带进度的两次调用出现过progress stream listened/cancelled,不带进度的两次调用一条都没有——这也是"四个方法"而不是"一个方法加回调"的价值所在:不需要进度时不会白建一条事件通道。

Q8:示例为什么要删掉integration_test?

因为它在场时鸿蒙构建会直接失败(AdaptorError 00303231: The srcPath is not a relative path: .../packages/integration_test/ohos):工具会把 dev 依赖里的插件都登记给 OHOS,而integration_test这个 SDK 包没有 ohos 模块。移除它只影响示例自身的集成测试,插件的功能验证改由演示页 + 宿主 HTTP 服务完成。


十、本篇用到的库

项值
适配仓库https://atomgit.com/oh-flutter/large_file_handler
上游仓库https://github.com/DenisovAV/large_file_handler
上游版本0.5.2(MIT,对应 main7987f70;上游未发布 tag)
适配 TAG0.5.2-ohos-1.0.0-beta.1
适配分支feat/ohos_large_file_handler_0.5.2
平台目录ohos/(插件 HAR)、example/ohos/(示例工程)
通道方法通道large_file_handler、事件通道file_download_progress

依赖写法(写死 TAG,不跟分支):

dependencies:large_file_handler:git:url:https://atomgit.com/oh-flutter/large_file_handler.gitref:0.5.2-ohos-1.0.0-beta.1# 鸿蒙宿主必须显式引入,否则插件里的 getApplicationDocumentsDirectory() 会抛# MissingPluginExceptionpath_provider_ohos:^2.2.1

验证环境

项值
Flutter for OpenHarmony SDK3.44.9+ohos-0.0.1-canary1
Dart3.12.2
DevEco Studio26.0.0.621(OpenHarmony SDK API 26)
设备Pura X View 模拟器,HarmonyOS 7.0.0(26.0.0) Beta2,ohos-x64
构建产物example/build/ohos/hap/entry-default-signed.hap

复现命令

# 1. 在宿主机器上起一个限速 HTTP 服务(40MiB 随机文件,约 2.5MB/s)node.agents/tools/http-test-server.mjs httpd--port 8899--throttle 2500000# 2. 构建(PUB_CACHE 必须与工程同盘;模拟器是 ohos-x64)$env:PUB_CACHE ="E:\pub-cache"cd example flutter pub get flutter build hap--debug--target-platform ohos-x64# 3. 安装并启动hdc shell power-shell wakeup hdc install-r build/ohos/hap/entry-default-signed.hap hdc shell aastart-a EntryAbility-b com.example.large_file_handler_example# 4. 依次点击:复制资源(带进度)→ 与 asset 逐字节比对# → 下载 URL(带进度)→ 重新检查文件状态hdc shell snapshot_display-f/data/local/tmp/lfh.jpeg hdc file recv/data/local/tmp/lfh.jpeg.# 5. 取原生日志hdc shell hilog-x|Select-String"LargeFileHandlerPlugin"

欢迎加入 CPF-Flutter 鸿蒙社区:https://atomgit.com/CPF-Flutter

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询