ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

Flutter跨平台适配鸿蒙:拼豆记录本开发到打包全记录

Flutter跨平台适配鸿蒙:拼豆记录本开发到打包全记录 先把我这个项目的来龙去脉说清楚。我做了个“拼豆作品记录本”应用用的是 Flutter 跨平台方案目标平台直接对齐鸿蒙同时保留 Android、iOS、Windows 的原生扩展能力。简单说这是一款给拼豆爱好者的工具类应用你把拼好的像素图在网格画布里一块块点上色应用记录色号、尺寸、用时最后导出成带色号备注的图片方便二次复刻或分享。之所以用 Flutter是因为它一套 Dart 代码能同时输出多端UI 一致性好、自定义绘制能力强而鸿蒙这边社区适配已经能跑通全集成了正好适合这种中小型工具应用。初期我把功能范围收得很窄作品管理、网格设计器、色号库、导出分享就这四块。拼豆用品的核心诉求不是炫酷动效而是“快速记录、精准还原”所以性能重点放在画布的格子渲染上。后面越调越顺也踩了不少鸿蒙适配的坑我把整个开发过程整理成一篇能照着抄的实操记录适合已经在 Flutter 里写过几个项目、又想把应用搬到鸿蒙上的开发者参考。1. 整体设计与技术选型思路1.1 为什么拼豆记录本需要跨平台开发拼豆玩家最常见的场景是看到一张像素图 - 按色号一个个点豆 - 熨烫定型 - 拍照分享。这里的痛点是作品会积压、图案会遗忘纸质稿很容易丢。我调研了一圈市面上的像素画应用发现多数偏向游戏和头像生成要么色号体系跟实体拼豆品牌对不上要么不支持自定义网格尺寸几乎没有专门给实体拼豆玩家做“记录与复刻”的工具。所以这个应用的价值点不在“画图”本身而在“记录结构”每一格存的是色号索引而不是颜色值这样可以根据不同品牌色卡动态映射每一份作品保存完整 JSON后面打开能继续编辑、缩放、局部修改。这时候跨平台就是刚需了。拼豆用户里用安卓的、用苹果的、用鸿蒙的都有我不可能给每端写一套原生。Flutter 的 CustomPaint 和手势系统又恰好能撑住上万格画布的流畅编辑比在 Web 上做 canvas 再包壳更轻也比原生控件更好统一渲染效果。鸿蒙近两年用户体量涨得很快但我赌它主流开发框架还不算完全稳定直接用 ArkTS 写一遍再移植到安卓成本很高。用 Flutter 做一层中间抽象未来就算鸿蒙 API 改了Dart 层画的 UI 也不受影响。1.2 鸿蒙适配在 Flutter 生态里的现状不少人对“Flutter 上鸿蒙”的第一反应是能跑吗我的回答是能跑但别把它当成安卓模拟的那套。鸿蒙运行 Flutter 用的不是官方主分支的 API而是 OpenHarmony 适配层社区里最常见的是 flutter_flutter_ohos 这个发行版。它保留了 Flutter 引擎层和 Dart Framework 层的能力但渲染后端与原生接入方式不同。你在鸿蒙端看到的 Flutter 页面其实是被包进一个 ohos 原生工程里的原生壳加载 Flutter 引擎ArkTS 那边可以用 PlatformView 或 Overlay 承载 Flutter 渲染结果。跑通这套要特别注意版本匹配。不是随便拉个最新 Flutter 就能编鸿蒙的你要按 flutter_flutter_ohos 仓库维护的 tag 来拉对应版本。我项目里用的是 Flutter 3.22.x 对应的一版 ohos 适配DevEco Studio 用 5.0.3 releaseOpenHarmony SDK 用的 API 12。这套组合我在真机上验证过稳定性尚可。你要是直接把官方 3.24 主分支拿来配 ohos大概率会在编译期直接报缺符号。1.3 应用功能模块的收敛过程我一开始规划了四个页面首页作品墙、编辑画布、作品详情、设置。后来发现“作品详情”可以跟“编辑画布”合并因为用户看详情其实是想复刻复刻就要能看到每一格色号并支持修改。于是产品形态收敛成三个模块作品墙网格列表支持下拉刷新、搜索、按色号筛选画布编辑器核心模块支持格子绘制、橡皮擦、填充、选区、缩放、移动色号管理内置几大常见拼豆品牌色卡支持自定义添加、收藏、最近使用这种收敛对开发很关键。画布编辑器一旦承担了“详情展示”的职责我就不用再为详情页维护一套独立的网格渲染逻辑代码量直接少三分之一。2. 鸿蒙开发环境搭建与工程配置2.1 Flutter SDK 和 DevEco Studio 的版本匹配这是鸿蒙 Flutter 开发最容易翻车的环节我踩了几次才稳定下来。先说一下结论别用官方 flutter SDK 直接配 OpenHarmony要用 flutter_flutter_ohos 的分支。我的搭建步骤是这样的下载 flutter_flutter_ohos 对应版本解压后把bin目录加进 PATH。我习惯把 SDK 放/opt/flutter_ohos这种固定路径避免 DevEco 和 CI 反复找。安装 DevEco Studio 5.0.3并在 SDK Manager 里勾选 OpenHarmony SDK 和 hms 相关组件。注意 OpenHarmony 的 SDK 目录结构是Sdk/openharmony里面有ets、toolchains、previewer等子目录。配置环境变量。鸿蒙的 Flutter 工程编译时需要通过LOCAL_SDK_HOME或 DevEco 指定的 SDK 路径来寻找工具链我在~/.bashrc里加了export DEVECO_SDK_HOME/path/to/DevEcoStudio/sdk export OPENHARMONY_BASE_SDK_HOME/path/to/DevEcoStudio/sdk/openharmony export PATH$PATH:/path/to/flutter_ohos/bin不配这些变量最常见的表现是flutter doctor完全不显示 OpenHarmony 工具链或者flutter build ohos报 “Cannot find SDK”。2.2 创建鸿蒙壳工程Flutter 官方当前没有把 ohos 作为一等平台写进flutter create所以流程是先生成 Flutter 标准工程再用适配版的命令行工具补出鸿蒙壳。我用的是这套流程flutter create perler_app cd perler_app flutter build ohos注意这里顺序不能反。第一次flutter build ohos时脚本会在工程根目录生成一个ohos目录里面是完整的鸿蒙原生工程结构包含entry/src/main/ets、entry/src/main/resources、oh-package.json5等。之后你每次更新 Flutter 侧代码直接重新执行flutter build ohos就会把 Dart 代码打进 har并由鸿蒙工程壳负责安装运行。有一个很值得注意的点生成出来的鸿蒙原生工程里MainAbility 和 EntryAbility 是自动包好 Flutter 容器了的。你要改的通常是模块名、图标、启动页这些配置文件不要去动ets里跟 Flutter 引擎初始化相关的代码除非你要做原生能力扩展。2.3 真机调试与无线调试配置鸿蒙真机默认不开放 adb你需要先用 DevEco Studio 信任设备。开启开发者模式后在“关于本机”连点版本号七次再进“开发者选项”打开“USB 调试”和“无线调试”然后# 先用 USB 连一次记录设备序列号 hdc list targets # 开启无线调试后手机会显示一个 ip:port hdc tconn ip:port工作中我用hdcHarmonyOS Device Connector比较多它就是鸿蒙版的 adb。无线调试在鸿蒙上比安卓稳可能是没有安卓那些繁杂的 RSA 指纹确认流程只要同一个局域网内基本秒连。但有个细节DevEco 的无线调试默认端口每次重开都会变脚本里不要写死端口要么用hdc tconn每次动态解析要么用 USB 做持续开发。2.4 模块依赖与权限声明鸿蒙应用要在module.json5里声明权限这一步容易漏。拼豆应用需要保存作品图片到图库所以要加{ name: ohos.permission.WRITE_IMAGEVIDEO, reason: 用于保存拼豆作品导出图, usedScene: { abilities: [EntryAbility], when: inuse } }很多 Flutter 插件在安卓上会自动往 Manifest 里合并权限但鸿蒙的module.json5不会自动合并忘了加权限的表现就是运行时静默失败不弹窗不报错。我调试导出功能时发现图片总保存不了查了半天才意识到是权限声明缺失。建议把ohos.permission.WRITE_IMAGEVIDEO和ohos.permission.READ_IMAGEVIDEO一次性都配上。3. 核心功能拆解与关键模块实现3.1 拼豆画布的数据结构设计拼豆画布本质是一个二维矩阵每个格子的值是该位置的色号 ID。我一开始直接用ListListint存后来发现大网格做序列化和撤销重做时有性能隐患就改成分段存储。最终设计成画布只记有效格子空白格不占数组位。class PerlerCanvas { final int rows; final int cols; final Mapint, int _cells; // key: row * cols col, value: colorId int get(int r, int c) _cells[r * cols c] ?? 0; void set(int r, int c, int colorId) { int key r * cols c; if (colorId 0) { _cells.remove(key); } else { _cells[key] colorId; } } }这样做的原因很实际29x29 的方板理论上有 841 格但一幅真实作品往往只有几十上百个别色块用稀疏存储能大幅减少 JSON 体积。而且 Map 的随机访问复杂度是 O(1)跟二维数组没差别但序列化时只要输出有值的格子可读性高很多。3.2 网格编辑器的渲染性能优化拼豆格子动辄上千用 Widget 堆格子是灾难。我试过用GridView.builder渲染 29x29 的网格在低端手机上拖动时明显掉帧。后来全部改用CustomPaint一次性绘制。绘制逻辑分三层底色层整体背景通常用浅灰模拟拼豆模板网格线层按当前缩放比例画横竖线缩放小时隔几格合并一条豆子层遍历_cells只画有值的位置每个豆子画成圆角矩形中间再叠一个小高光模拟立体感class PerlerPainter extends CustomPainter { final PerlerCanvas canvasData; final double cellSize; override void paint(Canvas canvas, Size size) { // 画底色和网格线 // 遍历 _cells 画豆子 } override bool shouldRepaint(PerlerPainter oldDelegate) oldDelegate.canvasData ! canvasData || oldDelegate.cellSize ! cellSize; }这里我强调一个细节shouldRepaint里一定要比较cellSize。因为画布缩放时网格线的间距和豆子大小都会变化如果只比较数据集你会看到网格线纹丝不动。这也是很多 Flutter 绘制组件“放大后就糊”的根因之一。3.3 颜色管理与色号映射拼豆色号不能直接用颜色值作为唯一标识。同一款颜色在不同品牌里编号差别很大比如红色在某些品牌里叫 401另一些叫 09用户还可能自购非品牌豆。所以数据库表里必须有独立的color_id主键再关联品牌的色号字符串和对应的 RGB 值。我建的表结构大概是字段类型说明color_idINTEGER PK全局色号 IDbrandTEXT品牌如 MARD、NABEbrand_codeTEXT品牌色号如 20nameTEXT颜色名如 黑色red / green / blueINTEGER用于实际绘制的 RGBis_customINTEGER用户自定义标记画布存储的colorId是这个color_id而不是 brand_code。这样好处很明显如果品牌色卡做了纠错只要改品牌表的映射所有旧作品都能自动用新颜色显示不需要迁移画布数据。3.4 作品导出与 JSON 持久化导出功能分两路一路导出 JSON 源文件另一路导出带色号标注的图片。JSON 我用dart:convert直接写到了应用文档目录文件名用作品 ID 加时间戳避免重名覆盖。图片导出则是用RepaintBoundary包裹画布再调用toImageRenderRepaintBoundary boundary key.currentContext!.findRenderObject() as RenderRepaintBoundary; ui.Image image await boundary.toImage(pixelRatio: 3.0); ByteData? bytes await image.toByteData(format: ui.ImageByteFormat.png);导出图的分辨率要注意。拼豆作品如果只有 29x29 格直接按 1:1 导出会很模糊。我把每个格子渲染成 24 物理像素再乘上pixelRatio: 3.0最后生成的图够在社交平台看清色号不算糊。3.5 Navigator 状态保持与页面切换的坑热搜词里有一条很典型“flutter navigator切换页面后,会丢失状态吗”。我自己也栽过编辑画布时切到后台或者按 Home再回来发现画布被重建正在进行的格子上色操作丢了。原因不是 Navigator 本身而是编辑页被系统回收后重建StatefulWidget里的PerlerCanvas对象还在但CustomPaint因为shouldRepaint判断失误没重绘。我采用的方案编辑页状态不入栈而是用IndexedStack保活Stack( children: [ Offstage(offstage: _currentIndex ! 0, child: WorkListPage()), Offstage(offstage: _currentIndex ! 1, child: EditorPage()), ], )这样编辑页一直活在 Widget 树里切页只是调整 offstage不会被销毁。代价是内存里始终保有一个画布实例但对单作品编辑这种场景完全够用。你要是同时编辑多个作品可以考虑PageStorageKey配合AutomaticKeepAliveClientMixin目的都是把画布数据留在内存里。4. 鸿蒙特有的适配细节与原生通信4.1 EventChannel 和 MethodChannel 的鸿蒙端对接纯 Dart 层面能做完大部分功能但有一些能力必须走原生保存图片到系统相册、读取设备型号、获取屏幕分辨率、唤起系统分享面板。这时候就需要通道通信。鸿蒙端原生代码用 ArkTS 写。以保存图片为例Flutter 侧先定义 MethodChannelstatic const MethodChannel _channel MethodChannel(perler_app/save_image); Futurevoid saveImage(String path) async { await _channel.invokeMethod(saveToGallery, {path: path}); }鸿蒙端在 EntryAbility 的onCreate里注册好 MethodCallHandler接收 Dart 发来的请求import { MethodCall, MethodCallHandler } from ohos/flutter_ohos; import { abilityAccessCtrl } from kit.AbilityKit; const handler: MethodCallHandler (call, result) { if (call.method saveToGallery) { const path call.arguments[path]; // 调用 ImagePacker 保存到相册 result.success(true); } };这里要说一个容易踩的坑鸿蒙的 MethodChannel 参数解析方式跟 Android 不太一样call.arguments在 Android 里可以直接当 Map鸿蒙则要先做一次类型转换否则读到的是object一调属性就崩。你最好在 Dart 层统一传 JSON 字符串原生侧再 parse多一道转换但非常稳。4.2 PlatformView 嵌入原生视图的场景如果你的应用要内嵌鸿蒙原生控件比如用系统的视频播放器预览教程、或者展示一个原生地图组件那就得用 PlatformView。在 Flutter 里创建 PlatformView 的接口是PlatformViewLink但鸿蒙适配版的核心逻辑有别于安卓的 TextureLayerBridge。以播放器为例注册原生视图得在鸿蒙工程里实现PlatformViewFactory然后在 Flutter 侧写UiKitView( viewType: harmony_video_player, creationParams: {url: ...}, creationParamsCodec: const StandardMessageCodec(), )对拼豆应用来说我目前没有用到 PlatformView因为画布是纯自绘的导入图片也用系统相册通道解决。个人建议除非真有必要否则尽量不用 PlatformView鸿蒙上它还在优化滚动嵌套时容易遇到触摸事件抢焦点的问题。4.3 Impeller 渲染引擎在鸿蒙上的兼容处理“flutter impeller” 这个热搜词背后是一个真实痛点。Flutter 从 3.7 开始逐步用 Impeller 替代 Skia在 iOS 和 Android 上口碑不错但在鸿蒙适配版上Impeller 对 OpenHarmony 图形栈的支持没有原生平台那么稳。我实测下来部分设备上开启 Impeller 会出现图片闪烁、阴影绘制异常甚至白屏。排查思路是先在真机上跑一下flutter run --enable-impeller或--no-enable-impeller对比画面稳定性。如果问题只在开启 Impeller 时出现直接在鸿蒙壳工程的module.json5里指定渲染引擎{ abilities: [ { name: EntryAbility, metadata: [ { name: flutter_rendering_engine, value: skia } ] } ] }有的版本支持在AndroidManifest.xml里加flutter.io.engine_flags但鸿蒙的壳工程不读这个你要看 flutter_flutter_ohos 当时版本的配置说明。我这里直接改成 skia 后画面闪烁问题消失了代价是部分高斯模糊动画的性能比 Impeller 差但拼豆应用的动画需求很低完全可接受。4.4 下拉刷新与列表性能的平衡作品墙我用了RefreshIndicator来实现下拉刷新这个在 Flutter 里开箱即用但鸿蒙上列表滚动时有个体验问题列表项图片是异步加载的快速滑动会频繁触发图片解码导致帧率波动。我的处理方式列表项用ListView.builder不要整个列表直接生成所有 Widget图片缩略图缓存到内存 Map避免同一幅图反复FileImage解码在itemBuilder里对缩略图做cacheWidth限制一般设成 200 就够Image.file( File(path), cacheWidth: 200, fit: BoxFit.cover, )cacheWidth这个参数新手很容易忽略。它告诉 Flutter 解码时直接把图片缩到指定宽度省内存也省 CPU。没加这个参数之前29x29 导出的图片可能只有几百 KB但在列表里每个 item 都按原图解码照样卡。5. 常见问题排查与打包避坑实录5.1 “flutter build ohos” 时 Gradle 插件报错这个热搜词我印象太深了“you are applying flutters main gradle plugin imperatively using the apply s”。报错原因是 Flutter 新版 Gradle 插件改成要求用声明式插件而适配鸿蒙的壳工程里某些初始化脚本还在用老式apply方式加载插件。我当时把工程里的ohos/build.gradle打开发现文件顶部写的是apply plugin: com.ohos.flutter改成新式写法之后就能编过了plugins { id com.ohos.flutter version 1.0.0 apply false }但是注意鸿蒙壳工程的 Gradle 文件结构跟安卓不同build.gradle里plugins块的写法要跟 DevEco Studio 的版本对应。不要盲目照抄网上安卓的修复方案改坏了一时半会看不出来报错会变成 “Could not find plugin”。5.2 打包时java.lang.AssertionError: Could not close ...这类的报错通常伴随 “internal error” 出现来源是 Gradle 在打包阶段对资源或 class 文件的 IO 操作失败。常见原因有三个多进程并发访问.gradle缓存导致文件锁冲突杀毒软件或系统策略锁了中间文件鸿蒙的Build目录有残留旧产物跟新产物冲突我的排查次序是rm -rf ./ohos/build ./ohos/.cxx rm -rf ~/.gradle/caches/transforms-* ~/.gradle/caches/build-cache-* flutter clean flutter pub get flutter build ohos清完基本都能过。如果还报错就把 DevEco Studio 里的构建进程杀掉因为 DevEco 和命令行的 Gradle daemon 同时跑一个工程也会触发文件锁问题。5.3 数据持久化选型sqflite 还是 drift拼豆记录本的核心数据是作品列表和画布内容数据量不大单作品顶多几十 KB。我用的是sqflite鸿蒙上通过sqflite_common_ffi跑 SQLite实测稳定。热搜词里提到 db4sDB Browser for SQLite这是一个开源跨平台工具Windows、Linux、macOS 都能打开 SQLite 文件。我在调试作品 JSON 和数据库时直接用 DB4S 查看perler_app.db的表结构方便定位字段插入错误。如果你的应用多端同步、增量更新复杂可以上drift它是类型安全的 ORM但学习成本高一些。拼豆这种单机工具应用sqflite 足够。5.4 鸿蒙抓包与调试技巧开发阶段排查网络请求我用 Charles 抓包。鸿蒙 4.2 以后对 HTTPS 证书信任管得严直接把 Charles 根证书装在系统里不行得走“无线调试 将证书导入用户目录”的方式。我的做法是手机开启无线调试hdc tconn ip:porthdc shell进入命令行把抓包证书 push 到/data/local/tmp在“安全”设置里手动安装证书要注意鸿蒙应用如果不开“允许明文流量”http请求会被拦截统一用https最省事。Charles 代理地址就填你电脑的局域网 IP端口 8888。鸿蒙的无线调试配置好后直接就能在 Charles 里看到 Flutter 发出的 HTTP 请求包括图片上传和反馈提交。5.5 Flutter 集成到已有原生工程的处理有个热搜词是“安卓原生项目嵌入flutter页面”这个思路在鸿蒙上同样适用。如果你不想让应用完全 Flutter 化而是想在一个已有的鸿蒙原生工程里加几个 Flutter 页面做法是先单独建 Flutter 模块编译产出.har或.so在鸿蒙原生工程里用FlutterEngine和FlutterViewController加载模块通过FlutterEngine的MethodChannel建立模块间通信但我得提醒一句Flutter 页面嵌进原生工程两边的导航栈是独立的你在 Flutter 内部用Navigator.push时原生壳不知道返回键的拦截逻辑要自己接。拼豆应用我没有走这条路整体 Flutter 鸿蒙壳的模式更省心以后要是做原生化再重构。6. 实操过程中的心得体会把整个项目从技术验证到跑在鸿蒙真机上我最大的体会是Flutter 在鸿蒙上的适配已经不是“能不能跑”的问题而是“怎么跑得稳”的问题。真正耗时间的不是 Dart 侧的业务代码而是环境匹配、原生壳配置、通道通信这类平台工程问题。你最好把环境版本固定住写进 README别随手升级 Flutter 和 DevEco否则一套组合一变之前的适配可能白做。拼豆作品记录本这种应用核心壁垒不在 UI 多花哨而在数据模型是否贴合真实使用习惯。我的色号映射、稀疏矩阵、JSON 分段存储这些设计都是在真机上压榨出来的。你如果也做同类工具我建议先做出安卓版把逻辑调顺再专门花一个版本做鸿蒙适配混合开发容易一步踩两个坑。最后分享一个可能对你有用的小技巧作品导出图片时除了 PNG我还会同时输出一份带色号的 CSV 文件一行一行的色号清单。很多拼豆玩家是拿实体材料包对照着配豆的他们不需要打开手机看图纸上一排编号照着找更快。功能不大但搜索时多一个“导入校准”的场景实际用户留存比预期高不少。这个思路也能迁移到其他创作类工具里值得保留。
返回列表