环境搭建指引: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');三个细节决定了适配的形状:
targetPath是相对的,由插件自己用path_provider的getApplicationDocumentsDirectory()拼成绝对路径再交给原生;- 带进度与不带进度是四个独立的方法(不是"传个回调"),它们共用同一条事件通道
file_download_progress; - 进度值就是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 -> boolpubspec 里的平台声明是这样的:
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 平台列表有没有ohos | android, 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.ets | HAR 出口 |
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: pluginClass4.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,文件=0x5065536 正好是块大小,指向"从第二块开始读错"。改成每次都传绝对偏移(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 SDK | 3.44.9+ohos-0.0.1-canary1(Dart 3.12.2) |
| DevEco Studio | 26.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 = 6be2cf5ea35faaaf388faecc8d55b4935be4e00488d6cc3f0e387165178a76aa7.2 实测数据
| 操作 | 文件 | 大小 | 应用内 SHA-256(前 16 位) | 期望值(宿主侧) |
|---|---|---|---|---|
| 复制资源(带进度) | sample_1mb.bin | 1048576 | 86a786ac32eb8523… | 86a786ac32eb8523…(HAP 内资源) |
| 下载 URL(带进度) | download_40mb.bin | 41943040 | 6be2cf5ea35faaaf… | 6be2cf5ea35faaaf…(宿主文件) |
配合界面上的进度统计:
| 操作 | 进度点数量 | 序列 |
|---|---|---|
| 复制资源(带进度) | 16 | 6, 12, 18, 25, 31, 37 … 100(1MiB ÷ 64KB = 16 块) |
| 下载 URL(带进度) | 101 | 0, 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):
下载完成后的最终状态:
八、已知限制
- 资源复制依赖
getRawFd():资源必须随 HAP 打包(Flutter 资源的默认落点就是resources/rawfile/flutter_assets/)。 - URL 下载在应用进程内完成:不申请后台任务权限,也不做断点续传(本库的 Dart 接口没有这两个语义);应用退到后台后能否继续下载取决于系统策略。
- 进度事件数量与上游不同:块大小 1KB → 64KB、相同百分比去重,可观测的序列一致但事件更少。
- 示例移除了
integration_test:见 6.1,这是示例侧取舍。 - 示例里多了
crypto依赖:只用于演示页在设备上算 SHA-256(纯 Dart)。 - 示例资源是 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) |
| 适配 TAG | 0.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 SDK | 3.44.9+ohos-0.0.1-canary1 |
| Dart | 3.12.2 |
| DevEco Studio | 26.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