Flutter如何真正跑通OpenHarmony:家庭相册应用实战与踩坑全记录
发布时间:2026/10/17 12:13:52 锦皓数字建站

总有人问我Flutter 到底能不能正经跑在 OpenHarmony 上而不是只会打一个 Hello World。这个问题我原来也给不出特别确切的答案直到我拿一个家庭相册 App 去逼了一把 flutter_for_openharmony 这条技术路线。最后不仅跑通了还把最麻烦的“回忆列表”做完了。这篇文章我会完整记录从工程搭建、媒体权限、照片数据桥接到回忆列表聚合算法和卡片 UI 实现的完整过程包括那些不实测根本发现不了的坑和对应的解法。如果你也想在 OpenHarmony 设备上用 Flutter 做一个偏数据展示的本地应用这篇可以直接当参考手册用。1. 家庭相册为什么成了我的 OpenHarmony Flutter 试金石1.1 用真实需求逼出问题我一开始其实只是想验证一件事Flutter 那套开发体验在 OpenHarmony 上到底能保留多少。跑个 Hello World 太容易了容易到什么都证明不了。真正要看的是三件事系统能力能不能通过平台通道打通、长列表滚动还能不能保持流畅、原生图片资源能不能被 Flutter 侧正常消费。家庭相册 App 恰好把这三件事全占了。它要访问系统相册里的照片要展示数量不小的图片列表还要在列表基础上做时间聚合和视觉卡片。每个环节都绕不开系统底层能力和 Flutter UI 的协作是比普通业务 App 更硬的试金石。我身边不少人在 OpenHarmony 上做应用时第一反应是用 ArkUI 重写一遍。这当然没问题但如果你已经有一套 Flutter 业务代码或者团队成员更熟悉 Dart重写成本就比较高了。家庭相册这种偏本地存储、偏展示、交互不算复杂的应用正好适合作为 Flutter 跨端能力在 OpenHarmony 上的验证场景。1.2 Flutter 跑在 OpenHarmony 上到底是怎么回事首先要明确一个认知Flutter 跑在 OpenHarmony 上不是把 Flutter 当成 WebView 套壳也不是把 ArkUI 和 Flutter 混在一个页面里。我用的这套社区适配方案本质上是把 Flutter 的 Dart 虚拟机、渲染引擎、Widget 体系都编译成 OpenHarmony 能调用的原生库然后以一个原生的页面容器作为入口。整条链路大概是OpenHarmony 的 Ability 启动后加载 Flutter 引擎Dart 代码负责全部业务和渲染逻辑Flutter 页面最终绘制在系统的 Surface 上。ArkTS 部分只承担两件事一是应用入口和生命周期二是把系统能力通过平台通道暴露给 Flutter比如读相册、取缩略图、拿文件路径。这说明 Flutter 不是来替代 ArkUI 的而是和 ArkTS 分工。一个家庭相册 App 里相册权限申请、媒体文件枚举、图片缩略图生成这种事放在原生侧做更稳页面结构、列表滑动、交互动画放在 Flutter 侧做更快。后面我所有代码都是按这个分工写的。2. 工程搭建从 flutter create 到 HAP 装进开发板2.1 环境清单和最容易对不上的版本我没有用普通的 Flutter 官方 SDK而是用了社区为 OpenHarmony 维护的适配分支。这个分支的核心改动在于它把 Flutter 引擎和工具链的产物从 Android/iOS 目标换成了 OpenHarmony 的 hap 目标。环境准备其实不难难的是版本对齐。我踩过的第一个坑就是 Flutter 版本和 OpenHarmony SDK 版本不匹配编译的时候 Native 符号对不上报错非常难看。我最后稳定下来的版本组合大概是下面这个样子不同适配分支的版本号会略有差异但组件角色是固定的。组件作用注意事项Flutter for OpenHarmony 适配 SDK提供 Dart 编译、Flutter 引擎、ohos 平台目标必须用 fork 分支不能用官方 FlutterOpenHarmony SDK提供系统 API、编译 hap 的基础能力API 版本要和 Flutter 分支适配IDE 和 hvigor 工具链工程管理、签名、构建 hap命令行和 IDE 内置都要能跑hdc 工具连接设备、安装 hap、查看日志设备端侧和工具版本也要对上如果你是第一次接触我的建议是先别追求最新版而是找一个别人已经验证过的组合。我就吃过这个亏一开始直接拉最新的适配分支结果折腾了两天环境。后来换成某个发布说明里明确写“可用于 OpenHarmony API 10/11”的稳定组合一次就过了。2.2 工程目录与构建命令工程初始化并不是直接flutter create .就能带出 OpenHarmony 平台目录。在我的这套 SDK 里需要先在 Flutter 配置中打开对应平台的支持再重新生成工程。我当时执行的命令大致是flutter config --enable-openharmony-platform flutter create --platforms ohos .执行完之后工程里会多出一个ohos目录。这个目录就是 OpenHarmony 应用工程里面包含entry模块也就是应用入口和原生桥接代码。目录结构大概长这样lib/ main.dart ohos/ entry/ src/main/ ets/entryability/EntryAbility.ets module.json5 build-profile.json5这个结构对我来说很重要的一点是lib目录完全属于 Flutter/Dartohos目录完全属于 OpenHarmony。业务代码尽量不往ohos目录里塞只把原生能力封装成 MethodChannel 接口。构建时可以直接在 IDE 里执行 Build Hap也可以用命令行flutter build hap --release产物生成之后用 hdc 安装到开发板或者模拟器上hdc install entry-default-signed.hap这里的“signed”很关键。OpenHarmony 应用默认要求签名签名配置不对hap 根本装不上去。这个问题我后面专门用一整节讲因为它太容易让人误以为构建流程坏了。2.3 一些运行时初始化细节Flutter 页面能不能显示出来取决于 OpenHarmony 侧入口是否正确加载了 Flutter 引擎。在EntryAbility里要做的事情是把 Flutter 引擎和Dart入口绑定起来。这部分代码写起来有点像 WebView 容器加载页面但底层不是 WebView而是 Flutter 自己的渲染。我建议把原生入口代码控制在最小范围。它只做生命周期透传和平台通道注册其他逻辑一律放 Dart 侧。这样出问题时定位非常清晰页面不显示查原生侧入口页面显示但功能不对查 Dart 侧逻辑。3. 相册数据链路媒体权限、URI 和 Dart 侧数据模型3.1 权限配置家庭相册第一步就是读相册。OpenHarmony 里读取图片视频是受控权限需要在module.json5里声明。我用到的权限是读取图片类媒体文件在配置里大概长这样{ requestPermissions: [ { name: ohos.permission.READ_IMAGEVIDEO, reason: 用于在家庭相册中展示本地照片, usedScene: { abilities: [ EntryAbility ] } } ] }只写在配置文件里还不够。这是用户级授权权限首次启动时必须动态弹出系统授权框。如果在 Flutter 页面起来之后才去请求权限会出现一个很难看的时序问题Flutter 页面已经渲染好但数据列表还是空的用户点授权之后再刷新。我的做法是在原生侧 Ability 的初始化阶段就触发权限请求等授权结果拿到后再启动 Flutter 引擎。这样 Flutter 侧看到的永远是一个已经授权的状态逻辑会简单很多。3.2 通过平台通道把图片信息送到 Flutter权限拿到后原生侧可以通过媒体库接口扫描设备上的图片然后把数据一条一条传给 Flutter。这里有一个特别重要的坑OpenHarmony 媒体库返回的资源路径是 URI不是普通文件路径。比如一个file://media/Photo/xxx这样的 URI你不能直接用 Dart 的File()去读因为 Flutter 侧没有权限解析这个 URI也没有对应的文件描述符。我的做法是原生侧负责把 URI 解析成真实可读路径或者干脆直接读取图片字节返回给 Flutter。简单说原生侧对一个图片对象做两件事返回元信息比如 ID、拍摄时间、宽高返回缩略图路径或者缩略图数据。Flutter 侧只跟这些解析后的内容打交道。平台通道的接口我定义得很克制static const _channel MethodChannel(family_album/media); FutureListPhotoMeta fetchPhotoList() async { final result await _channel.invokeMethod(fetchPhotoList); return result.map(PhotoMeta.fromMap).toList(); }原生返回的是包装好的 Map 列表Dart 侧解析成 Model后续所有相册功能都基于这个 Model 展开。3.3 照片实体的建模我经历了先写 UI 后建模型、然后反复改模型的尴尬阶段。最后沉淀下来的照片实体是下面这样的class FamilyPhoto { final String id; final String uri; final String thumbPath; final DateTime takeTime; final double latitude; final double longitude; final String album; const FamilyPhoto({ required this.id, required this.uri, required this.thumbPath, required this.takeTime, this.latitude 0, this.longitude 0, this.album , }); }[图片上传中请稍候...]真正让我决定建模型的原因是回忆列表的聚合逻辑需要频繁访问照片属性。如果不在一开始就把拍摄时间、地理位置这些字段解析出来后面做时间线分组就得反复请求原生通道性能完全没法看。4. 回忆列表实现连续事件聚合、时间标题和卡片 UI4.1 回忆的生成算法“回忆”这个词听起来很玄但落到代码里其实就是一次时间聚类。一张照片只有一个拍摄时间一堆照片按时间排序后会产生若干“密集时间段”。比如一家人周末逛公园可能两小时内拍了五十张之后隔了一周才有下一批。那么这五十张照片就是一个回忆下一批又是另一个回忆。所以核心算法就是先按拍摄时间排序再计算相邻照片的时间间隔间隔小于阈值就认为属于同一段回忆超过阈值就切断。我用的阈值是两天。为什么不是一天因为我发现周末连续两天的家庭活动很常见比如周五晚上到周日晚上中间会隔一个晚上如果阈值太短就会被拆成好几段回忆卡片就显得很碎。这个阈值不是死的你们可以根据自己的照片密度调整。ListPhotoMemory buildMemories(ListFamilyPhoto photos, {int maxGapDays 2}) { final sorted [...photos]..sort( (a, b) a.takeTime.compareTo(b.takeTime), ); final memories PhotoMemory[]; var current FamilyPhoto[]; for (final photo in sorted) { if (current.isEmpty) { current.add(photo); continue; } final gap photo.takeTime.difference(current.last.takeTime).inDays; if (gap maxGapDays) { current.add(photo); } else { memories.add(PhotoMemory(photos: List.of(current))); current [photo]; } } if (current.isNotEmpty) { memories.add(PhotoMemory(photos: current)); } return memories; }这个贪心算法是整个回忆列表的地基。调好时间阈值之后大部分回忆卡片已经能自动切分出来了。但只按“连续时间”切分还不够。家庭相册里的场景通常带有明显节假日特征比如过年、五一、国庆、暑假。完全依靠连续时间会把一个长假拆成几段所以我在此基础上又做了一次“月份层聚合”。思路是把已经生成的若干短回忆按同一个月合并如果跨月则按年份合并。这样卡片既能保留事件感又不至于碎到按小时分。4.2 封面的挑选与标题生成回忆卡片最重要的视觉元素是封面图。封面不一定是第一张照片因为第一张可能拍糊了或者只是随手一拍。我选择的是这段回忆里分辨率最高的一张作为封面理由很实际封面图往往要放大展示模糊图影响整个卡片质感。标题生成则按时间范围决定。同一句标题逻辑是这样的String memoryTitle(PhotoMemory memory) { final first memory.photos.first.takeTime; final last memory.photos.last.takeTime; if (first.year last.year first.month last.month first.day last.day) { return ${first.year}年${first.month}月${first.day}日; } if (first.year last.year first.month last.month) { return ${first.year}年${first.month}月; } return ${first.year}年${first.month}月-${last.month}月; }[图片上传中请稍候...]这个标题比“2024-05-01”这种纯日期格式更有人味。用户看到的是“2024年10月”“2023年1月-3月”配合封面照片很容易就回想起那段时间发生了什么。4.3 列表页的 Flutter 实现回忆列表的 UI 我没有用太复杂的设计而是做成了纵向卡片流。每一张卡片包括三部分左侧封面缩略图中间时间标题和照片数量右侧一个进入详情的小箭头。核心代码其实不长ListView.builder( itemCount: memories.length, itemBuilder: (context, index) { final memory memories[index]; return MemoryCard(memory: memory); }, )真正花时间的是卡片内的图片处理。Flutter 侧拿到的缩略图路径是原生解析过的加载时我强制加了cacheWidth避免高像素原图把内存吃满。Image.file( File(photo.thumbPath), cacheWidth: 720, fit: BoxFit.cover, )这一步非常关键。如果不限制裁剪宽度OpenHarmony 设备上那些几千万像素的照片会直接让列表内存暴涨。我后面单独开一节说这个坑因为它不是小问题。回忆卡片之间我有一个经验性的排版间距列表上下边距保持 16卡片之间保持 12。距离太小卡片之间会互相干扰视线距离太大滚动时又丧失连续感。这个数值可以按真机效果调但方向上要注意别把列表做成密集网格。5. 实测踩坑缩略图内存、权限时序与安装签名5.1 缩略图必须原生生成我最开始图省事直接让原生侧返回图片的真实路径然后 Flutter 侧通过Image.file去加载。结果开发板直接黑屏重启日志里全是 OOM 报错。原因很简单我拿到的不是缩略图而是原图路径。一张照片动辄几千像素宽在列表里同时存在几十张每个都按原尺寸解码内存直接爆炸。正确姿势是让原生侧生成缩略图。OpenHarmony 的图片接口可以按目标尺寸采样返回一张适合列表展示的小图。我按卡片宽度约 240 dp 去采样内存占用立刻降了一个数量级。这里分享一个判断标准列表页里不允许加载宽度超过 1024 像素的本地图片。任何超过这个尺寸的图片都应该在原生侧预先缩好再交给 Flutter。5.2 权限请求回到 Flutter 的时序问题权限问题在前面讲过一部分但这里必须再强调一次因为它非常隐蔽。一开始我把权限请求放在 Flutter 侧触发通过平台通道调原生方法。原生的授权弹窗是异步的用户还在思考点不点时Flutter 侧代码已经继续往下走了。等用户点完授权Flutter 又刷新了一次数据这时候原生媒体库才刚准备好结果。整个流程容易造成二次刷新甚至出现明明授权了但第一次加载始终是空列表的假象。我的解法是把权限请求完全挪到 Flutter 引擎启动之前。原生 Ability 在onCreate阶段请求权限结果拿到后再初始化 Flutter 引擎。这样 Flutter 业务代码永远不用关心权限状态只需要在界面里显示“没有照片”或者“照片加载中”。这个改动让代码结构清爽很多。Dart 侧少了一个状态机原生侧只多十来行代码。5.3 签名与 hdc 安装构建和安装这一步我踩的坑最冤枉hap 构建成功后hdc 安装一直报failed to install详情信息指向签名验证失败。当时我以为是构建配置写错了反复检查工程配置折腾了一下午。后来才发现是调试证书过期了。OpenHarmony 应用签名机制比 Android 更严格调试签名也有有效期。如果你隔了一段时间没碰这个工程重新安装时突然失败第一个要查的往往不是代码而是签名证书还在不在有效期内。处理办法很简单重新生成调试证书更新 IDE 里的签名配置然后 clean 重新构建。这一步做完安装过程基本就顺了。除签名之外hdc 连接不上设备也是常见问题。开发板需要开启开发者调试模式hdc 版本也要和系统匹配。这个没什么花活老老实实按工具链版本对齐就行。6. 下一步可做的人脸聚类与地点聚合6.1 更聪明的回忆聚合现在的回忆列表已经能跑但离我理想中的家庭相册还有距离。我想做的下一步是在时间聚合之上加入人脸聚类。OpenHarmony 系统有一些视觉能力可以通过原生侧对图片做检测把同一张脸归到一组。这样回忆就不再只是“某个月的某几天”而是真正围绕人物展开比如“宝宝第一次走路”“奶奶生日”。这类功能体验差别极大但复杂度也不是一个量级需要把人脸特征库、相似度计算、索引存储都考虑进去。如果暂时不想碰人脸可以先做地点聚合。照片的经纬度信息已经进入模型了按地理位置粗略聚合再把地图上的点串起来也能做出很有记忆感的“旅行轨迹”。6.2 个人经验回看这个项目让我最有获得感的地方不是把一个 App 跑通了而是验证了一条可复用的技术路线Flutter 的跨端能力确实能延伸到 OpenHarmony但前提是必须接受原生侧和 Dart 侧的分工。照片访问、权限、缩略图这种系统能力老老实实在原生侧做页面、交互、聚合逻辑放心交给 Flutter。凡是我不守这条规则、试图走捷径的地方最后都还回去了。这套家庭相册 App 的工程和回忆列表实现后面我会继续迭代。你要是正在折腾 Flutter 和 OpenHarmony 的适配最值得参考的其实就是那条平台通道边界划清楚后面就顺了。
锦
锦皓数字建站
深耕本土企业品牌数字化升级,专注原创端正雅致商务官网,从视觉设计到稳定运维全程保驾护航。