news 2026/10/3 3:51:37

Flutter跨平台鸿蒙开发实战:手账便签收藏应用的技术取舍

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
Flutter跨平台鸿蒙开发实战:手账便签收藏应用的技术取舍

看到“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 路线 B:ArkTS 原生重写核心模块

鸿蒙自己的 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(): Promise<void> { 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 Record<string, 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 同步,让素材在手机、平板、电脑之间流转;二是加更多纸张模板,以插件形式下载,把“便签纸收藏”从单纯的素材管理变成模板商店。后面这两块如果再动手,我还会回来继续写续篇。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/10/3 3:51:10

CS5523替代LT8911实战:MIPI转eDP桥接芯片替换指南

1. 为什么我会在量产项目里把LT8911换成CS5523先说说我手头这个项目的背景。客户要做一个工业级触控显示方案&#xff0c;主控SoC只有MIPI DSI输出&#xff0c;面板却是eDP接口的1920x1080 IPS屏。桥接芯片是绕不开的选择。最初我直接照搬之前的方案用了LT8911&#xff0c;毕竟…

作者头像 李华
网站建设 2026/10/3 3:50:53

OpenShell替换Windows开始菜单:效率与自由度的终级实践

如果你还在为Windows 10/11那套居中排布、图标自动堆叠、搜索要点好几层的开始菜单头疼&#xff0c;那我强烈建议你试试OpenShell。它是一款免费开源的经典开始菜单替代工具&#xff0c;前身是很多人熟悉的Classic Shell&#xff0c;后来由社区接手继续维护&#xff0c;改名Ope…

作者头像 李华
网站建设 2026/10/3 3:50:29

Docker Swarm Manager节点深度解析:Raft共识与高可用实战

聊Docker Swarm&#xff0c;与其一上来就纠结怎么把几十台机器批量拉进集群&#xff0c;不如先把Swarm Manager这个角色彻底吃透。这个系列上一篇从整体架构看过集群的全貌&#xff0c;这一篇就把Manager节点单独拎出来讲——它到底管什么、凭什么选Leader、挂了以后会发生什么…

作者头像 李华
网站建设 2026/10/3 3:50:09

C++ std::thread 使用方法

C是一种高级编程语言&#xff0c;被广泛用于开发高性能、大规模、复杂的软件系统。其中一个强大的特性就是多线程编程&#xff0c;而std::thread是C标准库提供的多线程支持的重要组成部分。std::thread是一个轻量级线程类&#xff0c;它允许程序员创建、启动、停止、等待线程。…

作者头像 李华
网站建设 2026/10/3 3:48:45

openrig 统一配置 Claude Code 与 Codex:YAML 声明式管理 AI 编程助手

1. openrig 到底想解决什么问题第一次看到 openrig 这个名字&#xff0c;很多人会以为是某个硬件项目&#xff0c;毕竟 rig 这个词在英文里常指“设备、装置、机架”。但结合它周边的关键词——Claude Code、Codex、YAML、Node.js——基本可以判断&#xff0c;这是一个围绕 AI …

作者头像 李华