1. 为什么我用Flutter去做鸿蒙版手账便签收藏应用
如果你跟我一样,既是手账和便签纸收藏爱好者,又是天天靠Flutter吃饭的开发,那大概率遇到过这种尴尬:在安卓上做得很好看的纸品管理App,朋友换到鸿蒙手机之后根本装不了,还得被追着问“能不能出个鸿蒙版”。我的方案很直接:把整个手账便签纸收藏应用用Flutter重构一遍,靠一套Dart代码同时覆盖安卓、iOS和鸿蒙设备。这篇文章就把这个项目从需求拆解、环境搭建、核心功能实现到真机踩坑的完整过程复盘一遍,给正准备做Flutter鸿蒙跨平台开发的同学提供一份能直接参考的实战记录。
1.1 鸿蒙应用开发的几条路线,我为什么选了Flutter
现在做鸿蒙应用,摆在面前的路线大致有三条:直接用ArkTS配合ArkUI写纯鸿蒙应用;用uni-app这种跨端框架;再就是用Flutter走跨平台路线。三条路我都实际踩过,说点自己的判断。
ArkTS是鸿蒙的原生开发语言,配合ArkUI的声明式UI,性能和对系统能力的调用确实最彻底。代价也很明显:如果你已有安卓和iOS的App,等于要额外维护一套完全独立的代码。对个人开发者和小团队来说,双端同步迭代的工作量是实打实的。uni-app的生态在Web和小程序方向很强,但如果你的核心逻辑是本地数据处理、复杂列表和自定义UI,它给我的感觉仍然偏“套壳”,原生观感差一截。
Flutter的优势在于渲染层完全自绘,不依赖系统控件。这意味着在安卓上长什么样的UI,在鸿蒙上基本还是那样,纸品纹理、渐变色、阴影这些在手账场景里很讲究的视觉效果,不用为每个平台重做一遍。而且Flutter和鸿蒙的结合已经不再是“能不能跑”的阶段,而是“能不能跑稳”的阶段,后面我会详细讲工具链的具体配置。
1.2 Flutter对鸿蒙的支持到底走到哪一步了
很多人的认知可能还停留在“Flutter跑在鸿蒙上需要各种魔改”的阶段。实际情况是,鸿蒙侧的Flutter适配已经以OpenHarmony社区维护的形式纳入官方仓库的适配分支,日常开发中拉下来的Flutter版本,已经可以用flutter create --platforms ohos直接创建鸿蒙工程。这一点非常重要:它意味着你可以用标准的Flutter工具链,而不是用某家厂商封闭定制的分支。
不过要注意,这个“支持”是针对OpenHarmony SDK体系的。你在DevEco Studio里创建的HarmonyOS工程,核心也是基于OpenHarmony SDK扩展出来的,所以整体开发链路是通顺的:Flutter负责业务和UI,鸿蒙侧只充当承载壳和系统能力出口。
从渲染引擎来说,Flutter在鸿蒙适配版上仍然以Skia为主,较新的版本也在逐步走Impeller路线。对便签纸收藏应用这种图片密集型场景来说,Impeller的优势体现在大量纹理贴图和高分辨率纸品照片的绘制性能上。如果项目跟着Flutter稳定版走,默认配置就能拿到不错的表现,暂时不需要过度纠结引擎细节。
1.3 手账便签纸收藏这个垂直场景,跨端价值在哪
选这个项目当范例不是随手定的。手账便签纸收藏有一个很特殊的痛点:纸品的价值判断高度依赖视觉细节——花纹图案、纸张纹理、克重、尺寸、品牌系列。这些信息只放在手机相册里根本没法管理,你需要一个能打标签、按品牌索引、区分收藏夹的数据结构。而这种本地数据密集型应用,恰好是Flutter的舒适区:数据模型统一,UI自由度高,不需要频繁调用系统级API,跨端迁移成本最低。
另一个现实原因是手账爱好者群体跨设备特征很明显。喜欢收藏便签纸的人,主力机可能是安卓、可能是苹果,也可能是鸿蒙设备,如果App只能跑在单一平台上,圈子传播起来会非常吃力。用Flutter做一套代码把三端都覆盖,是成本最低的答案。
2. 环境搭建:把Flutter跑到鸿蒙真机上
2.1 需要准备的工具链清单
先列一份我实际用到的工具清单,省得你在各种教程里拼图:
- DevEco Studio:鸿蒙应用的主力IDE,负责创建鸿蒙工程、签名管理和HAP打包。
- Flutter SDK:建议直接用包含OHOS平台支持的适配版。版本尽量跟着社区推荐走,太老的版本不支持
ohos平台参数,太新的预览版又可能和DevEco Studio的SDK版本对不上。 - HarmonyOS SDK / OpenHarmony SDK:DevEco Studio安装时会自动带,主要是放API、系统库和编译工具。
- 鸿蒙真机:开发者模式调试比模拟器可靠,权限弹窗、安全区、字体渲染这些适配问题,模拟器上看不全。
这套组合里容易翻车的点是版本匹配。我一开始把DevEco Studio升到最新,但Flutter适配版的编译产物还是按旧SDK生成的,结果HAP构建的时候死活过不去。后来统一按DevEco Studio内置的SDK版本来选Flutter适配分支,问题就消失了。建议你也先固定一套版本组合,别追求全都用最新。
2.2 配置步骤与常用命令
环境配置的核心就三件事:Flutter能识别鸿蒙工具链、能创建OHOS工程、能真机运行。按下面顺序走基本不绕路。
第一,安装DevEco Studio并启动过一次,让它把SDK组件都下载完。第二,配置Flutter适配版的环境变量,把flutter命令和DevEco的SDK路径关联起来。我是在环境变量里显式指定了SDK路径,避免flutter doctor跑去检测Android目录然后报错。第三,用标准命令创建工程:
flutter create --platforms ohos --org com.example paper_collection .创建完的工程结构里会多出一个ohos目录,这就是鸿蒙壳工程。日常开发还是写lib/下的Dart代码,真机运行直接用:
flutter devices flutter run -d <鸿蒙设备id>第一次跑会花比较长时间,因为要同时构建Dart AOT产物和鸿蒙侧原生壳,耐心等就好。如果在flutter devices里看不到设备,绝大多数原因是手机没有开启开发者模式,或者USB调试授权没弹出来。
2.3 真机调试链路
Flutter跑鸿蒙真机的调试链路和安卓类似:Dart代码改动走热重载,原生壳代码改动需要重新编译。不过鸿蒙下的热重载没有安卓那么及时,尤其在改到与平台相关的插件注册时,经常会“热重载后没变化”,这种时候直接R重启最实在,别浪费时间反复试。
调试日志方面,端侧日志可以从DevEco Studio的Log窗口看,Dart侧的debugPrint输出也汇聚在那里。我习惯把打印级别调好,不然便签纸详情页的图片加载日志会被一堆系统级调试信息淹没。建议在开发期间单独封装一个AppLogger,统一控制输出开关,排查问题会舒服很多。
3. 手账便签收藏类应用的领域建模与数据存储
3.1 便签纸、手账本、收藏夹:对象怎么拆
这个App最核心的业务对象就三个:便签纸(PaperItem)、收藏夹(Collection)和标签(Tag)。别急着设计一个面面俱到的数据库表,先把实体边界理清楚。
便签纸是一张具体的纸品,属性要能支撑收藏者的“决策价值”:
class PaperItem { final String id; final String name; final String brand; final String series; final String material; // 材质:和纸、特种纸、铜版纸 final int weight; // 克重,单位g/m² final String size; // 尺寸,如 80mm x 100mm final String color; final List<String> imagePaths; final Set<String> tagIds; bool isFavorite; final DateTime collectedDate; final String note; }收藏夹是用户的“手账本”,里面可以放多张纸品;一张便签纸也可以出现在多个收藏夹里,所以关系上要用多对多处理。我做了一个轻量的关联结构:Collection持有itemIds列表,这样查询逻辑简单,也不容易把数据模型做成一团乱麻。
3.2 纸品特征参数与标签系统
手账纸品玩家找纸的时候,最常问的问题是“有没有哪个品牌的和纸胶带是80g的”“有没有复古色系的方格便签”。所以光有字符串备注不够,必须把纸品特征做成可筛选的字段。
我落地了两层方案:一是把高频属性(品牌、材质、克重、尺寸、颜色)固化成独立字段,支持筛选器直接查询;二是用自由标签覆盖长尾需求,比如“限定款”“赠品”“绝版”。标签系统的数据结构设计成扁平标签加tagIds引用,而不是做复杂的标签分组,理由很简单:这个App的体量不需要那种企业级分类体系,扁平结构在手账收藏场景里足够灵活。
给纸品“贴画像”的体验很关键。详情页里我用了一个“纸品画像卡”组件,把品牌系列的logo色、常用场景标注、克重区间一次性展示出来。这些画像数据其实是静态的JSON映射加上用户自填信息拼接的,不需要额外后台。
3.3 存储方案:为什么我避开SQLite选Hive
这是我在这个项目里最想强调的一个选型决策。最开始我下意识想用SQLite的习惯,但马上意识到一个问题:SQLite在安卓和iOS上都有成熟的sqflite插件,到了鸿蒙上,这套原生插件的适配进度是个未知数。一旦原生插件不支持OHOS,我就得回头写一堆平台通道,那跨平台的意义就折了一半。
所以我选了Hive。理由非常直接:Hive是纯Dart实现的NoSQL数据库,不依赖任何原生代码,天然能在Flutter支持的每一个平台上跑。在鸿蒙上跑Hive,和在安卓上跑Hive,除了存储目录不同,没有任何额外适配成本。配合hive_generator生成适配器,读写实体对象的体验非常顺畅。
final box = await Hive.openBox<PaperItem>('papers'); await box.put(item.id, item);另一个考量的点是查询模式。这个App的搜索场景是“先全量扫描再内存筛选”,因为纸品收藏的规模一般就是几百到几千条,用Hive内存Map做全量过滤完全够快,没必要为这个量级引入SQL查询引擎。如果你未来要处理数万条以上的记录,再考虑迁移到数据库方案也不迟,领域模型做成这样,迁移成本是可控的。
4. 核心功能落地:列表、收藏与详情页
4.1 首页网格列表与瀑布流
便签纸收藏应用的主页是展示型的,用户第一眼要看的是“纸品图案”,不是文字信息。所以我用网格列表做主力布局,每张卡片只露出缩略图、品牌名和收藏状态,克重和材质这类参数点进详情再看。
网格实现上用GridView.builder,子项是一个自绘的PaperCard组件。关键点是图片缓存策略。便签纸的照片一般不只有一张,缩略图如果直接拿原图解码,列表会明显掉帧。我在保存图片时同时生成一套低分辨率缩略图,卡片只加载缩略图,等进入详情页再加载全尺寸原图。这样网格列表的滚动帧率稳定在正常水平,体验和原生应用没有明显差距。
为了兼顾“纸质纹理”的展示效果,卡片背景我用了一个自绘的细纹理层:用CustomPainter画了一层很淡的噪点,模拟纸张纤维的视觉感受。这个细节在安卓和鸿蒙上表现完全一致,也侧面验证了Flutter自绘渲染在跨端一致性上的优势。
4.2 收藏与取消收藏的全局状态同步
收藏功能是这类App的“心脏”。用户可能在列表页点星标收藏,也可能在详情页长按收藏,两个入口的状态必须实时同步。如果靠页面间传值来回通知,早晚会漏。
我把收藏状态提升到了全局Store里,每个PaperItem的isFavorite只是一个持久化快照,运行时的唯一数据源是Store中的收藏集合。任何入口触发的收藏变更,都通过同一个Action修改Store,然后由监听机制通知所有页面重建。这个模式后面在Provider章节会详细展开,这里先记住一个原则:不要在每个页面里各自维护一份收藏状态。
4.3 详情页的纸品画像组件
详情页是手账玩家“欣赏纸”的地方,功能分三块:全尺寸图片轮播、纸品参数表格、标签与收藏夹管理。
图片轮播我直接用PageView加缩放手势包,没有引入太重的手势库。纸品参数表格用自定义行组件渲染,参数名和值分左右两列,支持品牌名长文本自动换行。标签区域做成了Flow芯片列表,点击标签可以跳转到该标签的筛选结果页。
这里有一个体验细节:收藏夹的归属关系显示在详情页底部。用户查看一张便签纸时,能直接看到它被收纳进了哪几个手账本,点击进入对应收藏夹。这个“反向归属”功能实现上就是遍历收藏夹的itemIds,数据量小的时候性能没有任何压力。
4.4 搜索、筛选与排序
筛选器设计我用了组合条件面板:品牌下拉、材质多选、克重区间滑杆、颜色色块选择、只看已收藏。搜索走内存过滤,输入的关键词同时匹配名称、品牌、系列和备注。
List<PaperItem> _applyFilter(List<PaperItem> source, FilterCriteria criteria) { return source.where((item) { if (criteria.brand != null && item.brand != criteria.brand) return false; if (criteria.maxWeight != null && item.weight > criteria.maxWeight) return false; if (criteria.favoriteOnly && !item.isFavorite) return false; if (criteria.keyword != null && !_matchKeyword(item, criteria.keyword!)) return false; return true; }).toList(); }排序支持按收藏日期、品牌名称、克重三个维度。有一点需要注意:在Dart里如果直接在build方法里跑完整筛选逻辑,页面会频繁重建,所以我把筛选过程包在compute之外的内存操作里,并且对结果做了缓存,只有条件变化时才重新计算。对于几千条数据的量级,这个优化足够保证手感和流畅。
5. Provider状态管理在手账场景的具体用法
5.1 为什么选Provider而不是其他框架
Flutter的状态管理框架真的太多了,Bloc、Riverpod、GetX各有拥趸。这个项目我选Provider,核心原因是它和Flutter的开发心智最贴近:ChangeNotifier加ListenableBuilder,没有额外的概念负担。在鸿蒙适配这个前提下,选状态管理框架要考虑的不光是功能丰富度,还有第三方生态的兼容性。Provider本身是纯Dart实现,无原生依赖,在鸿蒙上不存在插件适配问题,这对跨端项目来说是很大的确定性。
Bloc的样板代码太多,Riverpod的学习曲线和编译期生成器在鸿蒙工具链下也有额外不确定性,GetX倒是轻量,但它在路由上的“万能魔法”反而和鸿蒙页面栈的适配逻辑可能冲突。综合下来,Provider是稳妥且够用的选择。
5.2 全局收藏计数与筛选联动的实现
我在Store里维护了一个收藏集合,对外暴露三个核心内容:收藏数量、收藏状态查询、切换收藏的Action。
class CollectionStore extends ChangeNotifier { final Map<String, PaperItem> _favorites = {}; int get favoriteCount => _favorites.length; bool isFavorite(String itemId) => _favorites.containsKey(itemId); void toggleFavorite(PaperItem item) { if (_favorites.containsKey(item.id)) { _favorites.remove(item.id); } else { _favorites[item.id] = item; } notifyListeners(); } }收藏列表页、详情页、AppBar上的收藏角标,全部监听这个Store。UI层只需要调用context.watch<CollectionStore>()就能自动响应变化。筛选条件里“只看已收藏”的开关,底层也是复用这个集合做过滤,所以任何入口的收藏改动,都会即时反映到筛选结果里。
5.3 组件间的通信方式:跨组件与跨页面
热词里提到的“Flutter组件通信”,这里顺带总结一下这个项目的落法。我处理组件通信时只用两种方式:
- 父传子:通过构造函数传参。比如卡片组件接收一个
PaperItem对象,详情页接收itemId去Store里取数据。 - 全局状态共享:通过Provider/ChangeNotifier。任何层级的组件要读取全局数据,直接
context.read或context.watch。
这个App里几乎不存在“兄弟组件互相传数据”的需求。如果遇到,正确的做法是把数据提升到它们共同的父级Store里,而不是让组件之间直接耦合。你真正追查bug时会发现,手账类应用的状态流非常直白:用户操作永远通过Action进入Store,UI永远通过监听响应Store,循环就两条线,不会乱。
页面路由我用的是官方Navigator,没有引入第三方路由框架。在鸿蒙上跑Flutter时,第三方路由包对页面生命周期和返回手势的处理可能和原生壳存在适配差异,官方Navigator虽然朴素的但最不容易出问题。页面监听用的WidgetsBindingObserver,通过AppLifecycleState判断前后台切换,比如从相册选图回来后刷新当前页面数据。
6. 鸿蒙真机适配:我踩过的坑和排查过程
6.1 权限与隐私弹窗导致的图片加载失败
鸿蒙的权限体系跟安卓不是一回事,这是我在真机上遇到的第一个硬骨头。最初我用的是通用图片选择插件的默认配置,从相册选便签纸照片时直接黑屏或返回空。排查后发现是鸿蒙侧的媒体读取权限没有在module.json5里声明,导致系统在底层直接把选择回调卡掉了。
解决方式有两层。第一层是在ohos目录的module.json5里补权限声明,媒体类权限要按鸿蒙的分类名写。第二层更省事:改用鸿蒙系统自带的PhotoPicker能力,通过系统相册选择器拿图片,这样用户在系统层面授权一次即可,App自身不需要主动申请媒体库权限,省掉了隐私弹窗适配和合规文案的麻烦。
这个坑的排查链路是:先确认图片选择在系统相册里能正常显示,再定位到权限声明缺失,最后改成系统Picker。以后你在鸿蒙上遇到“某个原生能力静默失效”,第一反应应该是去查module.json5的权限声明,而不是怀疑Flutter侧代码。
6.2 安全区、字体与沉浸式布局差异
这个坑来得比较隐蔽。我按安卓的习惯做了沉浸式状态栏,结果在鸿蒙真机上,底部手势条区域突然吃掉了一截操作空间,详情页的“加入收藏夹”按钮被顶到看不见。
本质原因是鸿蒙的avoid area(避让区域)在竖屏沉浸式下的默认行为跟安卓不完全一样。解决思路不复杂:布局底部预留安全区高度,用MediaQuery.padding拿到避让区域的尺寸。我单独封装了一个HomeSafeArea组件,统一处理顶部状态栏和底部手势条的高度,所有页面套用它,保证任何机型都不会遮挡核心操作按钮。
字体方面,鸿蒙界面的默认字体是HarmonyOS Sans,对中文的渲染笔画偏清晰,而安卓默认字体相对偏圆润。Flutter在鸿蒙上会优先按系统字体回退规则绘制,我实测下来中文字体表现正常,但个别英文数字组合的宽度和安卓略有差异。遇到弹性布局里文字被截断的情况,先检查是不是字体宽度差异导致,别一头扎进布局代码里瞎调。
6.3 列表卡顿:图片解码开销的优化
手账纸品预览图是我自己拍的,单张原图动辄十几兆。列表页刚接上数据时,滚动起来明显卡顿,掉帧能直观感受到。用Flutter的性能分析工具看,卡顿点集中在图片解码,网格里每张卡片都在解码大图,资源开销自然爆炸。
常规做法是限制分辨率,但我在实际测试里发现,光用cacheWidth只是缩了缓存尺寸,解码本身还是全尺寸先走一遍。真正有效的方案是:存图时生成缩略图。我在图片工具类里封装了generateThumbnail方法,在选图入库后立即生成宽高不超过600px的缩略图文件,列表和卡片只读缩略图,详情页再按需读原图。
优化之后,网格列表的滚动流畅度立刻上来了,内存占用也降了很多。这个优化思路在安卓和鸿蒙上完全通用,属于跨端Flutter应用的通用必修课。
6.4 打包HAP时遇到的构建错误
打包发布阶段,我在DevEco Studio里构建HAP时踩了一个构建配置的坑。报错信息指向Gradle插件被错误应用,排查过程发现,原因是工程里混入了从安卓模板复制过来的插件配置,导致鸿蒙侧的构建脚本识别到了不该出现的应用指令。
这类问题处理的核心思路就一条:回到鸿蒙壳工程的标准模板,别把安卓的构建经验直接套过来。我把ohos目录下可疑的Gradle相关配置和Flutter官方教程里的标准配置逐行核对,把多余配置删掉,再由Flutter命令重新生成HAP构建脚本,问题解决。
建议你打包前先跑一遍flutter build hap命令行,用它作为主要构建入口,DevEco Studio只负责签名和上架相关的操作。这样能最大程度避免IDE和Flutter工具链对同一工程的双重管理冲突。
最后分享一个实际操作中的经验:这类跨端项目,每改完一个平台相关配置,一定第一时间在真机上跑一遍全流程,别攒到最后统一验证。鸿蒙的真机适配问题有个特点——很多坑是“环境特定”的,你在模拟器上可能永远复现不出来。图片权限、安全区、构建脚本这三类问题,我都是靠真机验证才暴露出来的。把这个测试节奏固化进开发流程,比看任何避坑清单都管用。