Flutter for OpenHarmony 跨端开发实战:排序对话框与 Provider 状态管理

发布时间:2026/10/7 21:42:42
Flutter for OpenHarmony 跨端开发实战:排序对话框与 Provider 状态管理 最近我把一个用 Flutter 写的记事本 app 迁到了 OpenHarmony 上跑项目不大定位就是轻量级开源记事本但麻雀虽少五脏俱全。这两天刚把排序对话框做完从交互设计到状态管理再到平台适配前前后后踩了不少坑。今天把这段实战经验整理成文给同样在折腾 Flutter for OpenHarmony 的朋友做一个参考。先说结论排序对话框本身不难难的是把它放进一套跨端状态管理方案里还要保证在 OpenHarmony 平台上的交互和 Android/iOS 保持一致。这篇文章会从项目整体架构讲起拆解排序功能的需求设计、组件通信选型、Provider 状态管理接入方式最后整理我在编码和真机调试中遇到的真实问题所有代码都是可以直接抄走用的。1. 项目全景为什么在 OpenHarmony 上用 Flutter 写记事本1.1 这个技术组合的底层逻辑先啰嗦几句背景。OpenHarmony 的系统底层以 C/C 为主应用层推荐用 ArkTS 开发但这并不意味着只能用一种语言写应用。Flutter 作为跨平台 UI 框架通过自绘引擎把 Dart 代码渲染成原生界面天然有一种一套代码多端运行的优势。社区和厂商合作推进了 Flutter 对 OpenHarmony 的适配目前已经能支撑真实项目落地。我选择这个组合的核心理由有三点。第一团队里已经有人熟悉 Flutter再学 ArkTS 的成本不小直接用 Flutter 能把迁移成本压到最低。第二Flutter 的热重载在调试 UI 时实在太好用记事本这种界面交互密集的小项目迭代效率比原生高不少。第三Flutter 的状态管理生态成熟像 Provider、Riverpod 这些方案都是经过大量项目验证的写起来比从零搭一套状态框架稳妥。需要提醒的是Flutter 插件在 OpenHarmony 上并不是全部可用。比如涉及平台通道的摄像头、蓝牙、传感器等插件都要去查对应的 OpenHarmony 适配版本。我的做法是先列一个功能清单逐个确认插件支持情况再决定是否入坑。记事本这种项目只需要文件读写和基础 UI踩坑面相对小。1.2 轻量级记事本的功能划分与架构分层这个项目定位是轻量级所以我有意识地砍掉了云同步、数据库、复杂标签这些功能。核心就四件事记录笔记、编辑笔记、删除笔记、整理笔记。整理笔记靠的就是排序对话框它让用户能快速切换视角比如按最近编辑排、按创建时间排、按标题字母排或者把置顶笔记排在最前。架构上我分了三层。数据层直接用 JSON 文件存到应用沙箱用 path_provider 拿目录不引入数据库业务层是一个继承 ChangeNotifier 的 NotesModel持有笔记列表和排序模式负责所有数据变更和排序逻辑UI 层就是列表页、编辑页和排序对话框这几个 Widget。分层之后最大的好处是排序逻辑可以在 UI 之外单独测试不会一改界面就把排序弄坏。排序对话框放在这个架构里的位置很明确它是一个 UI 组件通过 Provider 读写 NotesModel 里的排序状态。这样设计之后排序模式不只在列表页生效以后如果加设置页也能直接复用同一个状态。1.3 排序需求的产品视角做排序之前要先想清楚用户到底需要什么。记事本场景里最常见的排序诉求是找到最近改过的东西所以按修改时间倒序是默认选项其次是按创建时间适合回顾历史记录再就是按标题排适合笔记多了之后按名字找置顶优先级则是一个附加条件用户在意的笔记可以钉在最上面。我在设计排序选项时定了一个原则每个选项必须能带给用户可感知的差异。如果一个排序方式和另一个在当前列表里几乎看不出区别那这个选项就是在给用户添乱。另外排序偏好要持久化用户上一次选了按标题排下次打开 app 还得是按标题排否则每次进来都要重新设置体验会打折扣。2. 排序对话框动手写代码前的关键决策2.1 交互设计确认式还是即时生效排序对话框的交互有两种常见方案。第一种是即时生效用户在对话框里点一个选项列表立刻按新规则排再点另一个列表又变整个过程中列表和对话框同时存在。第二种是确认式用户在对话框里选好点确定列表才变点取消则什么也不发生。我最终选了确认式但做了一点微调对话框里的选中状态是临时保存的点击确认才写入全局状态并刷新列表。这样用户有两层保护误触了某个选项不会马上污染列表还有反悔的机会。就像在外卖 app 里选口味选完可以先看看购物车再决定要不要下单而不是点一下辣度就立刻把餐下了。实现上这个交互需要对话框内部自己维护一个局部选中状态等用户确认后再把结果传出去。如果你用 Provider 直接把对话框里的选中状态同步到全局那取消按钮就名存实亡了。这是很多人写排序对话框时容易犯的第一个错。2.2 组件通信的五种姿势对比排序对话框要向列表页传递用户选了什么排序方式这本质上是组件通信问题。我整理了一下 Flutter 里常见的五种方案各有适用场景。方案优点缺点适用场景回调函数简单直接父组件传一个方法进来多层嵌套时代码会变丑子组件直接通知父组件Navigator.pop 返回值对话框标准做法语义清晰只能返回一次适合单次选择单选确认类对话框Provider / Riverpod全局状态共享任意组件可读需要学习状态管理概念排序模式多处使用EventBus解耦彻底事件满天飞排查困难模块间低频通知全局单例写起来最快不利于测试和维护临时方案不推荐排序模式这个状态不只是列表页在用。列表页要读它来决定展示顺序设置页以后也可能要读它来展示当前偏好编辑页返回后列表要按之前的规则继续排。这就是典型的多组件共享状态我直接用 Provider 把它提到应用级一劳永逸。2.3 对话框 UI 拆解Flutter 里做对话框的标准入口是 showDialog它会返回一个 Future可以拿到用户操作的结果。基础写法就是用 AlertDialog 搭骨架title 放标题content 放选项列表actions 放取消和确认按钮。选项列表我选用了 RadioListTile 组件。它的好处是自带单选的视觉反馈用户一眼就能看出当前选中的是哪一项。要注意的是 RadioListTile 在 Flutter 3.x 里有个新老版本的参数差异老版本用 groupValue 和 onChanged新版本开始推 RadioGroup如果升级 Flutter 版本后发现报错先查这一条。对话框的 content 部分如果选项超过五个建议外面包一层 SingleChildScrollView 加 shrinkWrap否则在屏幕较小的设备上可能出现溢出的警告。OpenHarmony 设备碎片化程度也不低这个细节最好提前处理。3. 核心代码实现排序对话框从 0 到 13.1 数据模型与排序比较器封装先定义排序模式和笔记数据模型。这两个是排序逻辑的基石写不好后面全是坑。enum SortMode { byUpdateTime, // 按修改时间倒序 byCreateTime, // 按创建时间倒序 byTitle, // 按标题正序 } class NoteModel { final String id; final String title; final String content; final DateTime createTime; final DateTime updateTime; bool isPinned; NoteModel({ required this.id, required this.title, required this.content, required this.createTime, required this.updateTime, this.isPinned false, }); }排序的核心是写一个多关键字比较器。这里有一个关键点如果置顶优先级要生效不能先按 isPinned 排序一次再按标题排序一次。因为 Dart 的 List.sort 并不是稳定排序的可靠保证连续两次 sort 的结果可能不是你想要的。正确做法是把多个条件写进同一个比较器一次排序搞定。ComparatorNoteModel buildNoteComparator(SortMode mode) { return (a, b) { // 第一优先级置顶笔记永远在最前 if (a.isPinned ! b.isPinned) { return a.isPinned ? -1 : 1; } // 第二优先级根据排序模式比较 switch (mode) { case SortMode.byUpdateTime: return b.updateTime.compareTo(a.updateTime); case SortMode.byCreateTime: return b.createTime.compareTo(a.createTime); case SortMode.byTitle: return a.title.compareTo(b.title); } }; }这段代码看着简单但有一个容易被忽略的细节按修改时间排序时我用 b.updateTime.compareTo(a.updateTime)这就是倒序。如果你写反了列表会变成最旧的在最上面用户会觉得排序是坏的。我在测试阶段就抓出过这种低级错误后来养成了一个习惯每个排序模式都配一个固定的验证数据集把时间故意写得参差不齐一眼就能看出顺序对不对。3.2 Provider 状态管理接入与数据持久化接下来是状态管理部分。NotesModel 继承 ChangeNotifier持有笔记列表和排序模式对外提供查询和修改方法。class NotesModel extends ChangeNotifier { ListNoteModel _notes []; SortMode _sortMode SortMode.byUpdateTime; ListNoteModel get notes List.unmodifiable(_notes); SortMode get sortMode _sortMode; void setSortMode(SortMode mode) { if (_sortMode mode) return; _sortMode mode; _sortNotes(); notifyListeners(); _persistSortMode(); } void _sortNotes() { _notes.sort(buildNoteComparator(_sortMode)); } void _persistSortMode() { // 用 shared_preferences 存排序偏好 // SharedPreferences.getInstance().then((prefs) { // prefs.setString(sortMode, _sortMode.name); // }); } Futurevoid loadNotes() async { // 从 JSON 文件加载笔记然后 _sortNotes } }这里我强调一下 context.watch 和 context.read 的区别在 build 方法里读取排序模式驱动 UI 重建应该用 context.watch在事件回调里触发数据变更应该用 context.read。如果你在 onChanged 里用了 watch会因为监听发生在非 build 阶段而报错。这也是 Flutter 状态管理里出镜率最高的报错之一。NotesModel 在应用启动时注册到顶层用 ChangeNotifierProvider 包裹根组件。排序模式默认按修改时间倒序加载完用户偏好后再切换。这样即使上次的偏好读取慢了一拍列表也不会闪一下默认顺序再跳变体验会稳很多。3.3 排序对话框完整代码实现排序对话框本身我写成一个独立的 StatefulWidget内部维护局部选中状态 _localMode。这样做的原因是用户点选项时只改对话框内的显示列表不会跟着乱动。确认时才把结果同步到全局。class SortDialog extends StatefulWidget { final SortMode currentMode; const SortDialog({super.key, required this.currentMode}); override StateSortDialog createState() _SortDialogState(); } class _SortDialogState extends StateSortDialog { late SortMode _localMode; override void initState() { super.initState(); _localMode widget.currentMode; } override Widget build(BuildContext context) { return AlertDialog( title: const Text(排序方式), content: SingleChildScrollView( child: Column( mainAxisSize: MainAxisSize.min, children: SortMode.values.map((mode) { return RadioListTileSortMode( title: Text(_sortModeLabel(mode)), value: mode, groupValue: _localMode, onChanged: (value) { if (value ! null) { setState(() _localMode value); } }, ); }).toList(), ), ), actions: [ TextButton( onPressed: () Navigator.pop(context), child: const Text(取消), ), TextButton( onPressed: () { Navigator.pop(context, _localMode); }, child: const Text(确定), ), ], ); } }在列表页打开这个对话框时需要把当前排序模式传进去。确认返回后把结果交给 Provider 统一处理。这里的组件通信链路是列表页 - 打开对话框 - 对话框返回排序值 - 列表页调用 Provider 更新全局状态 - Provider 排序列表并通知所有订阅者 - 列表页重建。每一步都是单向的思路非常清晰。Futurevoid _showSortDialog() async { final notesModel context.readNotesModel(); final selected await showDialogSortMode( context: context, builder: (context) SortDialog(currentMode: notesModel.sortMode), ); if (selected ! null) { notesModel.setSortMode(selected); } }如果你不想让调用的页面承担转交职责也可以在确认按钮里直接读取 Provider 来更新排序模式。但那样对话框就和全局状态耦合了复用性会下降。我的经验是保持对话框纯净它只管用户选了什么至于选完怎么处理交给调用方决定。这样排序对话框哪怕以后放进设置页也能直接用。3.4 列表页联动排序与滚动位置处理列表页的数据展示我用 Consumer 包裹。排序模式变化后Provider 会通知所有订阅者Consumer 对应的区域自动重建列表就按新顺序渲染了。ConsumerNotesModel( builder: (context, notesModel, child) { final notes notesModel.notes; if (notes.isEmpty) { return const Center(child: Text(还没有笔记点击右下角新建)); } return ListView.builder( key: const PageStorageKey(note_list), itemCount: notes.length, itemBuilder: (context, index) { final note notes[index]; return NoteListTile(note: note); }, ); }, )这里有一个很多人会忽略的细节ListView 的 key 里我放了 PageStorageKey。排序之后列表数据顺序变了如果没有这个 keyListView 可能会保留之前的状态导致滚动位置错乱。加了 PageStorageKey 之后列表能更准确地感知这是一个需要重新构建的列表。另外排序完成后让列表滚动到顶部其实是一种合理的用户预期。因为排序意味着视角切换用户大概率想从第一个结果看起。如果非要保留原有滚动位置反而会看到一堆不相干的中间数据容易让人困惑。4. Flutter for OpenHarmony 平台集成要点4.1 工程搭建SDK 分支与工具链Flutter 官方主线并不直接支持 OpenHarmony需要使用专门的适配 SDK 分支。实际操作时你从开源社区拿到 Flutter 的 OpenHarmony 适配版本按照文档把它配置为本地 Flutter SDK 路径然后用 DevEco Studio 打开工程里的 ohos 目录完成编译和签名后部署到真机或模拟器。这里要注意的是ubp 工具链会从 ohpm 拉取依赖首次构建时间会比较久需要有心理准备。另外 Flutter 工程的 pubspec.yaml 里依赖的插件在 OpenHarmony 环境下可能有一部分没有对应实现编译时如果报找不到符号第一步先去查插件是否提供了 ohos 平台实现而不是盲目升级插件版本。真机调试要配置签名OpenHarmony 设备的签名体系和 Android 不太一样需要申请对应的调试证书。这一步卡住的人不在少数我自己的经验是认真读官方文档里的签名流程不要凭 Android 的经验想当然。4.2 Flutter AAR 集成与混合开发如果你不是从零起新工程而是要在已有的 OpenHarmony 原生应用里嵌入 Flutter 模块那就需要走 AAR 集成路线。Flutter 提供 flutter build aar 命令可以把 Flutter 模块打包成 AAR 文件供原生工程引用。这个方案适合已有原生团队和原生代码库、但想逐步引入 Flutter 页面的场景。注意事项是 AAR 里要带齐所有 Flutter 依赖和插件注册信息原生工程侧需要配置引擎初始化参数。一旦遇到 Flutter 页面和原生页面互相跳转的需求建议提前规划好统一的命名路由不然会出现页面栈混乱的问题。4.3 权限配置与 XTS 认证准备OpenHarmony 应用需要在 module.json5 里声明权限。记事本 app 的文件读写如果只在应用沙箱内进行通常不需要额外申请存储权限但如果要做到笔记导出或共享就要按需声明相应权限。权限声明这里的原则是最小化原则不用的权限坚决不申请既减少审核麻烦也降低用户隐私顾虑。XTS 是 OpenHarmony 生态的兼容性认证测试套件如果应用要上架官方应用市场或者预装到特定设备需要过 XTS 认证。对个人开发者的开源项目来说现阶段重点是保证基础功能在 OpenHarmony 上的稳定性和兼容性比如应用能正常安装启动、核心页面能完成完整流程、退后台再回前台状态不丢等。这些基本功过了后面真要去走认证流程底子就会比较扎实。5. 常见问题与排查技巧实录5.1 e/flutter 报错的定位思路日志里出现 e/flutter (31173): [ERROR:flutter/runtime/dart_vm_initializer.cc(41)] Unhandled Exception 这类信息本质是 Dart 侧有未捕获的异常。在 OpenHarmony 真机上遇到这种日志我的排查顺序是先看异常类型是不是 MissingPluginException如果是通常是某个插件的 OpenHarmony 实现没有正确注册再看是不是异步任务的异常比如文件读写失败没有 catch最后才是业务代码里的逻辑错误。遇到这类报错不要想着屏蔽它老老实实把异常堆栈打出来。我习惯在 main 函数里挂一个全局的兜底异常处理把未捕获异常记录到日志文件这样线上复现问题时能拿到第一手线索。5.2 排序后 UI 不刷新的三个原因排序状态改了但列表不刷新我遇到过的原因有三个。第一Provider 里没有调 notifyListeners只改了数据订阅者感知不到变化。第二调用了 setSortMode 但传入的是当前值我在代码里写了 if (_sortMode mode) return这个守卫条件会在值相同时直接退出不会触发刷新。第三对话框内部状态覆盖了全局状态也就是说用户在对话框里点了一下对话框自己的 _localMode 变了但全局的 _sortMode 一直没变列表当然纹丝不动。排查这种问题最直接的办法是在 setSortMode 里加一行 debugPrint日志里能看到传入的排序模式再对比全局状态变化三步之内就能定位。5.3 中文标题排序乱序问题用 String.compareTo 对中文标题排序结果会按 Unicode 码点排这在中文语境下基本不可用。比如张三和李四用户期待的拼音序是 Li 在前 Zhang 在后但码点序会完全不同。解决方案是引入拼音转换库比如 lpinyin排序前先把标题转成拼音再比较。如果只按首字母排也可以只取拼音首字母作为排序键。这个方案会有多音字问题但对记事本这种轻量级场景已经足够。追求更高准确性的话可以引入专业的分词和拼音库但体重和复杂度会明显上升我建议按需取舍。5.4 下拉刷新与排序滚动冲突列表页我加了下拉刷新用 RefreshIndicator 实现。在 OpenHarmony 真机上测试时发现下拉手势偶尔触发不了刷新相关事件处理不如 Android 流畅。排查后确认是触摸事件响应区域的问题后来把刷新指标和列表滚动方向做了协调配置同时注意排序后刷新时列表要回到顶部避免刷新完成后用户看错位置。这里的经验是跨平台项目里任何手势相关的东西都不能只看桌面模拟器必须真机逐个平台过一遍。很多 UI 在模拟器上完美真机上就是另外一回事。写在最后排序对话框这个功能单独拎出来看就是几个 Widget 加一个枚举的事但放到 Flutter for OpenHarmony 这个跨端语境里就会牵扯出状态管理方案选型、组件通信设计、平台差异适配、持久化策略这些问题。我个人在项目里最大的体会是动手写 UI 之前一定要先把数据和状态流想清楚。我前面就是先写了排序对话框再补状态管理结果返工了一次费了不少时间。现在这个复杂的开源记事本项目还在继续迭代计划下一步加入笔记搜索和简单分类。排序对话框的代码到时候可以直接复用。如果你正在做类似的项目希望这篇文章能让你少踩几个坑尤其是 Provider 的使用方式和中文字符串排序那一段都是我在真机上验证过的经验。

关于本文作者

来自尧图内容编辑团队

尧图内容编辑团队 内容团队

尧图内容编辑团队

本文由尧图网络内容编辑团队执笔。团队由资深项目经理、前端工程师与设计师组成,所有内容均来自亲手交付的真实项目,先讲清问题、再给出可落地的解法。尧图深耕北京网站建设十年,服务过京华建材集团、智造科技等各行业客户,把一线经验沉淀为可复用的行业观察。

  • 十年建站经验,覆盖建材、制造、服务、文创等
  • 项目经理把关选题与事实准确性
  • 工程师与设计师联合撰写专业细节
  • 统一编辑规范,保证文风与排版一致
  • 每月复盘转化数据,迭代选题方向

延伸阅读

相关资讯与近期热门内容

深度阅读推荐

建站决策前值得细读的三篇

网站改版的5个关键决策
2024-08-12

网站改版的5个关键决策

什么时候该改版、改到什么程度、如何避免流量掉光,京华建材集团改版复盘给出答案。

获取专属建站方案

看完文章,把您的行业与预算告诉我们,免费获取一份量身定制的官网建设方案与报价。

立即免费咨询