恒美微站
首页
关于我们
建站服务
主题模板
案例展示
资讯中心
联系我们
Flutter跨平台鸿蒙开发实战:手账便签收藏应用的技术取舍
首页
资讯中心
/
Flutter跨平台鸿蒙开发实战:手账便签收藏应用的技术取舍
Flutter跨平台鸿蒙开发实战:手账便签收藏应用的技术取舍
发布时间:2026/10/3 3:51:37
看到“Flutter 框架跨平台鸿蒙开发”这个标题我第一反应不是“Flutter 终于支持鸿蒙了”而是“跨平台这个坑到底有多深”。我上一款工具类应用就是基于 Flutter 做的跨平台版本后来要适配鸿蒙设备时才发现框架和系统之间的适配远不是装个 SDK 那么简单。这篇就围绕标题里的“手账便签纸收藏应用”来聊说说我在实践中的取舍、踩坑与最终落地方案希望能给正在纠结“要不要用 Flutter 搞鸿蒙”的朋友一些参考。这个应用本身并不复杂手账爱好者需要一个能快速记录灵感、收藏好看便签纸、打标签分类的工具。但它涉及的典型问题很全——本地数据持久化、图片渲染、列表刷新、组件通信、页面状态保持。换句话说拿它当 Flutter 跨平台和鸿蒙开发的试验田再合适不过。1. 内容整体设计与思路拆解1.1 手账便签纸收藏应用到底要解决什么问题先把标题拆开看。“手账便签纸收藏应用”听起来有点文艺实际核心需求非常朴素用户看到一张好看的便签纸、一句有灵感的文案、或者一个手账排版范例希望能快速存下来将来随时翻出来参考。它和普通备忘录最大的区别是“收藏属性”也就是要有一个明确的收藏/取消收藏动作还要支持按分类浏览。我在设计时把功能收敛成四条主线新建/编辑便签、收藏与取消收藏、分类标签、搜索。前两条是刚需后两条是让内容“存得进去也找得到”。很多同类应用死掉不是因为功能少而是因为存了一堆东西却根本翻不到所以我特意把搜索和分类放在和新建同级的优先级上。这个应用的典型场景有两类一类是手账素材收集用户看到喜欢的排版就截图进来附上来源链接和自己的批注另一类是日常灵感记录类似便利贴随手写一句话打上标签。两类场景合在一起决定了数据模型不能太简单至少要有“标题、正文、图片列表、标签、收藏标记、创建时间”这几个字段。1.2 技术定位Flutter 跨平台生态与鸿蒙现状标题把“Flutter 框架、跨平台、鸿蒙”三个词放在一起很多人会理解为“用 Flutter 开发鸿蒙应用”。真实情况要复杂一些。Flutter 官方支持的平台里并没有鸿蒙社区确实有适配项目比如某些基于 Flutter 的鸿蒙 fork 分支或 OpenHarmony 的引擎适配但它们的维护节奏、API 完整度和官方 Flutter 版本之间存在明显gap。做跨平台开发的人都清楚Flutter 的核心价值是“一份 Dart 代码多端渲染”。它自带 Skia/Impeller 渲染引擎UI 不依赖系统组件所以理论上只要能把引擎搬到目标平台上就可以运行。鸿蒙的问题恰恰在于它不是 Flutter 官方工程里默认的 target platform你要么用社区维护的 fork 仓库重新编译引擎要么用混合方案搭桥。这带来的不确定性在真正接手一个产品时是要命的。我在项目启动前做了一个小验证拉一个最简单的 Flutter 页面用社区方案编译到鸿蒙设备上跑。结果页面能起来但一用到 PlatformView、EventChannel 这类原生交互功能版本匹配问题就暴露了。由此我得出一个结论如果你的应用只是纯 UI 展示Flutter 迁移鸿蒙可行只要涉及系统能力、文件读取、媒体库就必须先确认适配分支支持到什么程度。1.3 一个容易被忽视的问题引擎版本与渲染架构很多教程不会提 Flutter 版本差异对鸿蒙适配的影响但实际开发中这就是最大的暗坑。比如 Flutter 3.x 后 Impeller 在 iOS 上逐步取代 Skia 作为默认渲染引擎渲染性能和稳定性有所变化。社区做鸿蒙适配时往往是跟着某个特定 Flutter 版本走的你想升级 Flutter 小版本可能就得等适配分支同步。更麻烦的是Dart 语言、Flutter Framework、Flutter Engine 三者的版本必须严格对应其中任何一环错位编译期不一定报错运行期才崩溃。我在实践中是反过来做的先确定鸿蒙适配分支锁定的 Flutter 版本再围绕这个版本写业务代码而不是先写代码再往上怼引擎。这样虽然牺牲了一点 Flutter 新语法但换来了可预期性。对一个小工具应用来说稳定比炫技重要得多。2. 两条技术路线怎么选不后悔2.1 路线 A用 Flutter 直接编译到鸿蒙如果你已经有 Flutter 应用而且业务相对简单那路线A是合理的。你需要做的事包括把鸿蒙适配的 Flutter SDK 装好、用对应的引擎重新编译产物、处理原有第三方插件在鸿蒙上的缺失、重写获取图片和文件存储等原生能力。这里我建议先做一次插件清单梳理把所有依赖插件的鸿蒙支持状态列出来再决定要不要走这条路。我见过一个典型的失败案例团队花了三天让 Flutter 页面跑上鸿蒙结果发现原本用于选图的 image_picker 插件没有鸿蒙实现只好自己写原生插件又折腾一周。所以路线A真正考验的不是 Flutter 能力而是你对鸿蒙原生 API 的熟悉程度。如果核心功能都要通过自己写插件补Flutter 的跨平台优势就只剩 UI 层了这时就要重新算一笔账。2.2 路线 BArkTS 原生重写核心模块鸿蒙自己的 UI 框架是 ArkUI开发语言是 ArkTS。对新手来说从 Flutter 切到 ArkTS 有一个适应成本但并没有想象中高。ArkUI 的声明式写法、组件状态管理、生命周期模型和 Flutter 在很多地方神似比如State 对应 Flutter 里 setState 后的重建逻辑Prop 和 Link 对应父子组件传参和双向绑定。我上手两天后就能写出像样的页面了。路线B还有一个隐藏优势鸿蒙设备上的系统能力调用比如文件读取、媒体库、权限申请都是现成的一等公民 API。做手账应用最核心的“选图、存图、读图”功能在原生生 态里反而是成本最低的。Flutter 版反而要绕道插件插件还得等适配。2.3 不同场景下的取舍逻辑我的建议很简单存量 Flutter 应用且功能密度低走 A 路线做技术验证从零开发且目标平台以鸿蒙为主直接上 B 路线。标题里的“跨平台”真正合理的设计是分层业务模型和数据结构尽量跨平台通用UI 层在各端分别实现而不是字面意义的“一套代码跑到底”。拿手账应用来说我最后采取了折中方案数据层用 Dart 先写好再用 ArkTS 重写一遍核心逻辑UI 层完全用 ArkUI 实现。这样既不浪费跨平台的数据建模思路又避免把命运押在 Flutter 的鸿蒙适配进程上。如果你对“一次代码走天下”有执念建议做了原型测试后再决定不要一开始就赌。3. 数据模型与本地存储手账应用的“地基”3.1 设计便签数据模型时别把它当成普通 TODO手账便签和普通待办事项最大的区别在于内容形态多样可能是纯文字、一张图、也可能是图文混排。所以数据模型不能只存 title 和 content 两个字符串。我在建表时加了 images 字段用来存图片路径的 JSON 数组加了 category 字段存放分类名称加了 is_fav 布尔字段标记收藏状态加了 ts 时间戳用来排序和搜索。这个表结构简单但覆盖了应用的全部核心场景。建表语句用 SQLite 语义CREATE TABLE IF NOT EXISTS notes ( id INTEGER PRIMARY KEY AUTOINCREMENT, title TEXT, content TEXT, images TEXT, category TEXT, is_fav INTEGER DEFAULT 0, ts INTEGER );ArkTS 里用的是鸿蒙的 RelationalStore它的 SQL 语法和 SQLite 基本一致从 Flutter 迁移过来的开发者几乎不用改思路。这里有一个容易被忽略的点is_fav 不要用布尔类型建议用 0/1 整数。原因很简单后续你想扩展收藏分组、收藏时间、置顶状态时整数类型可以平滑演进布尔类型只能推倒重来。3.2 RelationalStore 还是 Preferences鸿蒙本地存储有两个高频选择Preferences 适合存键值对比如用户设置、最近搜索词RelationalStore 适合存结构化数据比如便签记录、分类表。很多新手图省事把所有数据全塞 Preferences单个 key 里塞一长串 JSON结果应用一上量就卡。手账应用的数据量虽然不算大但每篇便签可能有图片、大段文字、多次编辑记录放在关系型数据库里更适合后续做查询和分页。我在实际项目里的分工是便签数据走 RelationalStore用户偏好和主题设置走 Preferences。两者混用并不冲突反而能让各自的查询压力降到最低。给刚入门的朋友一个建议只有一条记录要存时用 Preferences一旦出现“列表页”、“筛选”、“按时间倒序”这些动词就直接上 RelationalStore别犹豫。3.3 便签纸的“纸感”渲染思路手账应用如果只是白底黑字就失去了“便签纸”的味道。但这里我不想直接引用大截图因为会让安装包体积暴涨。更好的做法是动态绘制便签纸背景普通便签画横线手账页画网格收藏页画点阵。需要说明的是这一步在 Flutter 里可以用 CustomPaint 轻松实现在 ArkUI 里则要改用 Canvas 组件。虽然渲染 API 不同但画横线网格的逻辑是一致的。Canvas 绘制便签纸网格的伪代码大致如下// ArkTS 中获取 Canvas 上下文 private drawGrid(context: CanvasRenderingContext2D) { const spacing 24; const width this.displayWidth; const height this.displayHeight; context.strokeStyle #E8E4D8; context.lineWidth 0.5; for (let x 0; x width; x spacing) { context.beginPath(); context.moveTo(x, 0); context.lineTo(x, height); context.stroke(); } // 横线同理 }这个方案能明显降低包体积同时给用户一种“这真的是便签纸”的视觉暗示。如果你非要加厚的纸张纹理也可以放一张小尺寸无缝纹理图重复平铺但要注意图片资源的内存占用。鸿蒙开发里图像解码性能不如成熟方案纹理图过大很容易在低端设备上卡动画。4. 实操过程与核心功能实现4.1 搭建 ArkTS 工程与最小权限配置新建 DevEco Studio 工程时建议选择 Empty Ability 模板不需要那些复杂的多模块模板。模块结构上我用了一个 Entry 模块承载全部 UI一个 Shared 模块放置工具类和数据模型这样后续如果真要接 Flutter 或其他端业务逻辑可以被复用。工程建完后第一步要做的就是配置存储和媒体权限。module.json5 里最小权限配置如下{ requestPermissions: [ { name: ohos.permission.READ_IMAGEVIDEO }, { name: ohos.permission.WRITE_IMAGEVIDEO }, { name: ohos.permission.INTERNET } ] }INTERNET 权限在你不需要网络请求时可以不申请但手账收藏大概率要同步图片来源链接所以我直接加上。权限申请的时机建议放在用户第一次点“选择图片”时不要一进来就弹窗否则容易触发隐私合规问题。真机上调试时很多问是因为没在设置里手动允许媒体访问导致的这不算 bug但排查起来很烦人。4.2 便签收藏功能与列表页代码骨架收藏功能的关键不只是把 is_fav 字段改成 1还要考虑“收藏列表”与“全部列表”的数据刷新一致性。我在实现时用一个简单的状态提升方案首页和收藏页都监听同一个数据源变化回调数据变更后重新查询数据库再统一更新列表。下面是收藏列表页的核心骨架Entry Component struct FavoriteListPage { State notes: NoteItem[] []; private dbHelper: RdbHelper new RdbHelper(); aboutToAppear(): void { this.loadFavorites(); } async loadFavorites(): Promisevoid { const result await this.dbHelper.queryNotes(is_fav 1 ORDER BY ts DESC); this.notes result; } build() { Column() { List({ space: 12 }) { ForEach(this.notes, (item: NoteItem) { ListItem() { NoteCard({ note: item }) .onClick(() { router.pushUrl({ url: pages/NoteDetailPage, params: { id: item.id } as Recordstring, string }); }) } }, (item: NoteItem) item.id.toString()) } .width(100%) .layoutWeight(1) } } }这段代码体现了 ArkUI 的几个关键点aboutToAppear 相当于页面每次显示前的加载时机适合在这里刷新列表router.pushUrl 传参时需要强转 Record 类型不然编译期不报错但运行时取不到参数。我在实际调试时被这个类型坑过一次页面跳转是成功的参数拿到的却是 undefined排查半天才定位到是参数格式问题。4.3 分类、搜索与下拉刷新分类和搜索在数据库层面都是 WHERE 子句的变体。分类是 field 某个值和 is_fav 组合搜索是 LIKE 查询。这里要注意一点字符串 LIKE 查询在数据量小的时候感觉不到差距但一旦便签数量超过几百条中文模糊查询的效率会明显下降建议后续引入倒排索引或分词方案。对小工具应用来说当前阶段用 LIKE 加时间倒序就够了架构上留好接口即可。下拉刷新在 ArkUI 里建议用 Refresh 组件包住 List注意刷新回调里要等数据彻底查完再结束 loading。在 Flutter 里改写的话就是 RefreshIndicator 加 onRefresh 回调思路完全一致。我在实践里发现一个细节刷新结束后列表会闪一下这是因为重新查询数据库后直接把整个数组赋值给了 State导致所有行都重建。精益做法是只更新有变化的数据项或者给列表项加上稳定 id让 ArkUI 的 diff 算法少做无用功。5. 常见问题与排查技巧实录5.1 便签纸图片在高分屏下走样图片走样是我在这个项目里第一个遇到的视觉问题。原因很简单没有做屏幕密度适配。鸿蒙设备屏幕拥有多种分辨率我在 Flutter 里可以用 MediaQuery 动态计算尺寸在 ArkUI 里要读取 display density 再对图片显示宽高做缩放。搜索热词里经常能看到“Flutter PlatformView 适配”本质也是跨端视图在不同像素密度下的尺寸不一致问题。解决方法是在加载图片前先读取目标容器的实际像素大小再按比例缩放图片。如果懒得动态计算直接把 image 组件的 objectFit 设置为 Cover也能避免明显变形但会裁掉图片边缘内容收藏素材图时未必能接受。我更建议存图时就把路径和宽高信息一并存进 images 字段显示时直接用预存的宽高比拟合。5.2 页面切换后状态丢失很多 Flutter 开发者都搜过“flutter navigator 切换页面后会丢失状态吗”因为 Flutter 的默认路由行为并不会自动保留所有页面状态。鸿蒙的 router.pushUrl 也有类似属性页面退到后台再返回时默认重新触发 aboutToAppear如果这里没有重新拉数据你刚才在详情页做的收藏动作往往不会反映到列表页。我在项目里的解决方法是详情页每次返回时把需要同步的字段作为 callback 传给列表页或者更简单一点在列表页的 aboutToAppear 里无条件重新查询一次数据库。对这个数据规模的应用来说重新查询的性能开销完全可以接受。不要迷信页面级状态缓存躬亲重查一遍数据库比任何缓存策略都稳。5.3 EventChannel 与原生通信的调试要点如果你还是在 Flutter 路线里做混合开发一定会碰到 Flutter EventChannel。热词里也有人问“flutter 组件通信”其实 EventChannel 适合原生向 Flutter 单向推送事件流比如系统媒体状态变化、网络状态变化MethodChannel 适合 Flutter 主动调原生方法并拿结果。很多人一上来就把两者用反结果通信时序乱成一锅粥。我在调试 EventChannel 时吃过一个暗亏需要在原生侧先注册通道、再往 Flutter 发事件如果 Flutter 侧的 listener 注册时机太晚第一条事件就会被吞掉。解决办法是实现一个带缓存的 EventChannel 包装类原生侧在 Flutter 注册完成前把事件先存到一个队列一旦注册成功立即补发。这个坑在官方文档里几乎找不到但实际开发中必然会碰到。5.4 小问题速查表现象可能原因解决思路列表页返回后不显示最新收藏页面没刷新数据aboutToAppear 里重查数据库图片加载后颜色偏灰图片路径写错或权限没弹检查媒体库权限和文件路径收藏数量不对is_fav 更新后没同步缓存统一走数据更新回调下拉刷新一直转圈Refresh 回调没及时结束确认查询完成后关闭 loading页面跳转参数 undefined参数类型不是 Record强制转换参数对象类型便签纸网格线发虚没有做屏幕密度适配根据 density 调整线宽这张表是我把开发中真实遇到的高频问题整理出来的贴给后面接手的人能少走一半弯路。6. 写在最后的个人心得我个人的实际体感是Flutter 跨平台开发理念很棒尤其适合快速验证 UI 和做原型但面对鸿蒙这种非官方目标平台时一定要多留一个心眼。所谓“跨平台鸿蒙开发”更务实的玩法是把业务逻辑抽象好UI 层再根据平台打磨而不是死守一套代码。这个手账便签纸收藏应用目前还只是单机版但我已经想好了后续扩展的两条路一是加 WebDAV 同步让素材在手机、平板、电脑之间流转二是加更多纸张模板以插件形式下载把“便签纸收藏”从单纯的素材管理变成模板商店。后面这两块如果再动手我还会回来继续写续篇。