去年搬新家的时候,我前前后后买了三十多件家具,从沙发、床垫到一把吧台椅,每件的购买日期、价格、保修期限都散落在不同的电商订单和纸质单据里。后期想查某件家具还在不在保修期,翻半天记录是常有的事。于是我做了一个家具购买记录App,用来登记每件家具的基本信息和购买凭证。项目本身不复杂,但开发过程中我选择了一条比较特殊的路线——用 Flutter for OpenHarmony 来跑这个跨平台应用,并在其中完整实现了设置功能。
这篇博文会把整个设置功能实现的核心过程拆开讲清楚,包括技术选型、工程搭建、UI 与交互、状态管理与持久化,以及我在 OpenHarmony 真机上跑 Flutter 应用时踩过的一堆坑。不管你是想用 Flutter 适配 OpenHarmony 平台,还是单纯想找一个中小型工具类 App 的设置页实现方案,这篇内容应该都能给你一些实在的参考。
1. 需求拆解:家具购买记录App里的设置功能到底要管哪些事
1.1 一个工具类App的设置页,不是简单堆几个开关
很多刚接触移动开发的读者容易把"设置功能"理解成"放几个 SwitchListTile 就行了",实际上设置页是整个 App 里最容易暴露架构问题的地方。因为它要和很多全局状态打交道——主题、提醒开关、数据存储方式、单位偏好,这些都会影响其他页面的行为和展示。
就家具购买记录这个场景来说,需求很明确:用户买了一件家具,需要记录品牌、型号、购买渠道、价格、购买日期、保修截止日、发票照片等;后期可能需要按照保修期快到期来排序查看,或者导出数据备份。围绕着这些核心玩法,我把设置功能拆成了五个模块:
- 外观设置:主题模式(跟随系统 / 浅色 / 深色),决定整个 App 的亮色暗色切换。
- 提醒设置:保修期到期提醒的总开关、提前天数(7 天 / 15 天 / 30 天),决定首页要不要在卡片上标出"即将过保"。
- 单位与格式:货币符号(¥ / $ / € / 自定义),日期格式(yyyy-MM-dd 还是 yyyy/MM/dd)。
- 数据管理:导出 CSV、备份到本地文件、清除全部数据。
- 关于页面:版本号、开源许可声明、免责说明。
这个设计思路是通用的,哪怕你今天要写的不是家具记录而是药品效期管理、租约到期管理,设置项的骨架也是类似的。关键是先把边界划清楚:设置功能不是独立模块,它是一堆全局配置项的聚合入口,任何一项改动都要能实时反映到其他页面。
1.2 为什么"设置"往往比主功能更考验工程质量
我在这个项目里的体会是,主功能页面(比如家具列表)只需要关注"数据怎么展示",而设置功能关心的是"数据怎么配置、配置怎么存储、配置如何驱动全局"。
具体来说有三个层面的复杂度:
第一,状态同步。你在设置页把主题从浅色切到深色,返回首页时,首页的 AppBar、背景色、卡片样式都得跟着变。如果用的是 setState 且状态留在设置页里,返回就失效了,需要全局状态管理来支撑。
第二,持久化。设置项如果只存在内存里,用户杀进程就全部丢失。必须落盘,而在 Flutter 里最常用的方案还是 shared_preferences,存储轻量级键值对。到了 OpenHarmony 平台,这个问题还多了一层适配插件是否可用的坑,后面详说。
第三,入口交互。多数 App 的设置页都在"我的"页面右上角或者抽屉菜单里,直接推一个 SettingsPage 即可。但在 OpenHarmony 上,页面路由和 Android 的 Intent 机制有差异,Flutter 侧的 Navigator 在 OpenHarmony 容器里是否完全可用,我在实测中也踩过小坑。
2. 技术选型:OpenHarmony应用开发的三条路线,我为什么选了Flutter
2.1 三条技术路线的横向对比
OpenHarmony 应用开发目前主要有这么几条路线,当时我做了个对比:
路线一,纯 ArkTS 原生开发。用 ArkTS 语言 + ArkUI 声明式框架,在 DevEco Studio 里开发。好处是官方支持最完善,对系统能力(比如通知、后台任务)的调用最直接;坏处是生态相对封闭,如果写过一个 Flutter/React Native 项目,切到 ArkTS 还是有一个重新适应语法的过程。而且我只想维护一套代码,后续如果还想上 Android 端,ArkTS 这条路就锁死了。
路线二,Flutter for OpenHarmony。Flutter 官方 SDK 本身只支持 Android、iOS、Web、Windows、macOS、Linux,OpenHarmony 不在官方支持列表里。但 OpenHarmony 开源社区在 Gitee 上提供了 flutter_flutter 的 OpenHarmony 分支,把 Flutter 引擎和框架层能力映射到了 OpenHarmony 的 Native API 上,实现了一部分跨端能力。这套方案的优点是可以沿用 Flutter 的 UI 写法和状态管理生态,最接近我已有的技术栈;缺点是需要接受社区分支的版本滞后性和部分插件不可用的问题。
路线三,uni-app / RN 等其他跨端框架迁移。其中 uni-app 有对鸿蒙生态的适配方案,但我在评估时发现当时对 OpenHarmony 的编译链路还不太成熟,而且我项目里需要用到本地通知和文件导出这两个原生能力,迁移成本并不比 Flutter 低。
综合考量后,我还是选了 Flutter for OpenHarmony。原因很简单:UI 一致性可控——Flutter 自带渲染引擎,在 OpenHarmony 上也能保持和 Android 端几乎一致的外观;我的业务代码可以共用一套 Dart;状态管理和持久化的框架心智负担也比较低。
2.2 Flutter for OpenHarmony的适配原理简析
为了后面排查问题不抓瞎,这里简单梳理一下 Flutter 在 OpenHarmony 上是怎么运作的。
Flutter 架构本身分成三层:上层是 Dart 编写的框架代码(widgets、rendering、material 等);中间是引擎层(Skia/Impeller 渲染、Dart 运行时、文字排版);底层是平台嵌入层(Platform Channel、Platform View、生命周期等)。传统 Android/iOS 平台,嵌入层分别由 Android Embedder 和 iOS Embedder 实现。
OpenHarmony 上的 Flutter 分支做的核心工作,就是实现了一个 OpenHarmony Embedder,通过 OpenHarmony 的 Native API(napi 接口)和 Ability 生命周期将 Flutter 引擎挂载到 OpenHarmony 应用中。也就是说,你的 Dart 业务代码可以基本不变,但是所有涉及原生能力的插件(比如 local_notifications、path_provider、shared_preferences)都必须有对应的 OpenHarmony 原生实现。如果没有,就需要通过 Platform Channel 自己写 napi 桥接代码。
这一点非常关键。我最初的设想是"Flutter 项目拿到 OpenHarmony 上直接就能跑",实际上得先把依赖插件的 OpenHarmony 适配情况摸一遍底。
2.3 选型之前必须先确认的版本兼容矩阵
在我实际动手前,版本匹配这件事值得花点时间确认。Flutter for OpenHarmony 的仓库是社区分支,它的基线 Flutter 版本往往比官方主分支落后几个版本。如果直接把官方 Flutter SDK 装的插件搬到 OpenHarmony 工程里,编译时经常报 kernel_snapshot 或者 platform_view 相关错误。
我当时的环境大概是这样:
| 组件 | 版本 |
|---|---|
| OpenHarmony SDK | 4.0 Release / API 10 |
| DevEco Studio | 4.0 及以上 |
| Flutter SDK(openharmony 分支) | 对应 Flutter 3.7/3.10 左右的社区 fork |
| Dart SDK | 随 Flutter 分支锁定,不单独混用 |
| 构建目标 | HAP(HarmonyOS Ability Package) |
这个矩阵的意思不是让你照抄,而是提醒一件事:不要拿官网最新版 Flutter SDK 去创建 OpenHarmony 工程,一定要用 OpenHarmony 社区维护的 flutter_flutter 分支,并且查清它对应的 Flutter 基线版本。版本选错,后面的坑是一个接一个。
3. 工程搭建实录:环境准备、项目创建与第一个页面的诞生
3.1 环境准备:DevEco Studio、Flutter SDK和OpenHarmony SDK的配合
工程搭建阶段是最容易翻车的,我花了大半天才把所有环境捋顺。
第一步,装 DevEco Studio。这里是 OpenHarmony 应用开发的主 IDE,相当于 Android Studio 之于 Android。装完以后进入 SDK Manager,安装 OpenHarmony SDK,选 API 10 那一套就行。这一步有几个值得注意的地方:一是安装路径不要带空格和中文;二是 IDE 自带的 SDK 目录和 Flutter 侧配置要一致;三是 Signature 相关配置在真机调试时要用到,建议提前在 IDE 里完成自动签名。
第二步,下载 flutter_flutter 的 ohos 分支。不能从 pub.dev 捞官方 Flutter SDK,而是要从 Gitee 上把 OpenHarmony SIG 维护的 flutter_flutter 代码拉下来,切到对应的 ohos 分支。拉完之后把解压目录的 bin 路径加到 PATH 里,然后运行 flutter doctor 验证。
第三步,创建工程。正常 Flutter 工程的创建命令是 flutter create,而这个分支下的创建指令需要额外加 OpenHarmony 平台支持。不同分支的写法略有差异,我用的命令类似这样:
flutter create --platforms ohos furniture_record_app执行完之后,工程目录里会多出一个 ohos 目录(类似于 android 目录的角色),里面是 OpenHarmony 的壳工程。也就是说,整体结构是 Flutter 的 Dart 层作为 UI 层,ohos 目录作为原生壳,最终构建时由壳工程把 Flutter 引擎和 Dart 产物一起打包成 HAP。
3.2 第一次构建:Gradle、镜像仓库与依赖下载
第一次构建 HAP 时,我遇到了全项目最典型的坑:Gradle 依赖下载超时。
OpenHarmony 壳工程的 Gradle 构建会从华为仓库拉取依赖,但在某些网络环境下非常慢,甚至直接超时。后来在 ohos 目录下的 build.gradle 或 settings.gradle 里把华为云镜像仓库加上,速度才恢复正常。我当时的做法是在 repositories 里补上:
maven { url 'https://repo.huaweicloud.com/repository/maven/' } maven { url 'https://mirrors.huaweicloud.com/repository/maven/' }另外还有一个点值得注意:如果要跑真机,需要在 DevEco Studio 里完成签名配置。OpenHarmony 的 HAP 是需要签名的,不签名装不到真机上。第一次构建时因为没配置签名,安装到设备上直接报 Parse 错误,我在没有报错上下文的情况下排查了挺久,后来才发现是签名问题。
3.3 跑通第一个空页面:验证 Flutter 渲染是否正常工作
工程创建成功并构建出 HAP 后,先在模拟器里跑一个最简单的 Flutter 页面。我当时写了一个只有一个 Scaffold + Text 的页面,确认 Flutter 的渲染管线在 OpenHarmony 上正常工作,再开始后续开发。
这一步非常有价值。因为它能帮你区分两种错误:一是我写的代码逻辑有问题;二是因为 Flutter for OpenHarmony 自身构建链路有问题。如果连最简单的页面都跑不起来,那就先别急着写业务代码,回头把构建环境再捋一遍。
我在这个环节遇到的一个印象比较深的报错,是 flutter run 直接报出 [ERROR:flutter/runtime/dart_vm_initializer.cc(41)] 之类的 Dart VM 初始化错误。排查下来发现,是 HAP 打包时没有正确打入 libflutter.so 的 ABI 版本,换到正确的 arm64-v8a 架构配置之后就正常了。这类问题在网上能搜到很多相似描述,但每个人的实际原因可能都不一样,建议优先检查壳工程里的 abiFilters 配置。
4. 设置页UI实现:分组列表、开关交互与主题适配
4.1 页面结构与分组布局:向上看齐系统设置页的交互习惯
设置页的 UI 实现,我没有自己做新奇设计,而是选择了用户最熟悉的"分组列表"模式。原因很直接:工具类 App 的设置页,用户的心智模型是"从上到下扫一遍,快速找到想改的项",效率比好看重要。
具体布局上,我用 ListView + 多个分组 Section 来实现。每组一个标题(比如"外观"、"提醒"、"数据管理"),组内用 Card 或 Container 包裹,每组之间留出间距。每一项用 ListTile 承载,右侧根据配置项类型放不同类型的控件:
- 开关类型:使用 SwitchListTile,value 绑定全局配置对象,onChanged 触发状态更新。
- 跳转类型:比如"关于页面"、"导出数据"这一类,右侧用 Icon(Icons.chevron_right) 做箭头指示。
- 选择类型:比如货币符号,点击弹出一个底部选择器或对话框。
这里有一个小组件设计上的细节:我封装了一个 SettingsGroup 和一个 SettingsItem,避免每个设置项都写十几行重复代码。SettingsItem 接收 icon、title、subtitle、trailing widget、onTap 回调,内部只做布局拼接,这样设置页的代码能保持很干净。
4.2 SwitchListTile 的坑:主题切换之后的页面刷新问题
设置页里最常用的是 SwitchListTile,但直接在 OpenHarmony 的 Flutter 分支上使用它时,我遇到一个比较隐蔽的问题:快速滑动设置项列表时,Switch 的滑块渲染偶尔会出现残影,看起来像是渲染层没有及时重绘。
后来我查了不少资料,这大概率是 Impeller 渲染引擎在老版本 Flutter(或者社区 fork 的渲染适配层)上的刷新同步问题。当时 Flutter 官方已经在新版本里做了不少修复,但 OpenHarmony 分支的基线版本较老,没有同步这些修复。我的临时解决方案是在构建时切换到 Skia 渲染 backend,也就是在运行参数里加上:
flutter run --enable-software-rendering --no-enable-impeller这里顺带解释一下,Impeller 是 Flutter 的新渲染引擎,取代 Skia 的很多绘制路径,但它对 OpenHarmony 这样非官方支持平台的适配还处于早期阶段,所以遇到渲染异常时,优先尝试关掉 Impeller 或者强制用软件渲染,往往能定位到是渲染引擎的问题还是业务代码的问题。
4.3 主题适配:浅色深色与"跟随系统"的实现
主题切换是所有设置功能里最典型的需求。实现思路分两步:
第一步,定义全局主题配置。我用一个 AppSettings 类保存 themeMode 字段,可选值为 system / light / dark。在 MaterialApp 的构造里,读取这个配置并赋值给 themeMode 属性:
MaterialApp( themeMode: settings.themeMode, theme: buildLightTheme(), darkTheme: buildDarkTheme(), home: HomePage(), )第二步,切换后的状态刷新。这一步取决于你用什么状态管理方案。我在这个项目里用了比较轻量的 ValueNotifier + ValueListenableBuilder,没有引入大型状态管理库。原因很简单:设置项状态的数量不大,AppSettings 是一个不可变对象,任何修改都重新生成一个新实例并通过 ValueNotifier 广播,页面中需要感知变化的组件用 ValueListenableBuilder 包起来即可。
class AppSettings { final ThemeMode themeMode; final bool warrantyReminderEnabled; final int reminderAdvanceDays; ... } class SettingsController extends ValueNotifier<AppSettings> { SettingsController(super._settings); }这个方案比 Provider 和 Riverpod 更轻,也更容易让新手看懂。如果你的项目大一点,需要跨很多页面共享配置,再用 Provider 也不迟。
5. 核心机制:设置项的存储、状态同步与数据管理
5.1 用 shared_preferences 存储配置:OpenHarmony上的插件适配方案
设置项必须持久化。Flutter 生态里最常见的方案是 shared_preferences,它是一个轻量级键值对存储插件,在 Android 上底层封装了 SharedPreferences,在 iOS 上封装了 NSUserDefaults。但问题来了:OpenHarmony 不是 Flutter 官方支持平台,依赖开源插件是否能用,全看社区有没有做适配。
我检查下来,OpenHarmony 社区确实有维护一套 flutter 插件的适配仓库,比如 shared_preferences 的 OpenHarmony 实现。使用方法跟官方基本一致,但需要在 pubspec.yaml 里通过 dependency_overrides 指向对应的 ohos 适配源:
dependencies: shared_preferences: ^2.0.0 dependency_overrides: shared_preferences: git: url: https://gitee.com/openharmony-sig/flutter_packages.git path: packages/shared_preferences/shared_preferences这里要特别提醒:dependency_overrides 是 Flutter 的"包依赖覆盖"机制,它会让 Pub 解析时用指定的 git 源替代 pub.dev 上的同名包。这种写法只在 OpenHarmony 工程里需要,如果你的项目还要构建 Android/iOS 版本,注意不要污染其他平台的构建。
SharedPreferences 的实际写入逻辑保持原样,比如我把整个 AppSettings 序列化成 JSON 字符串,然后以一个 key(比如 app_settings_v1)存储:
final prefs = await SharedPreferences.getInstance(); await prefs.setString('app_settings_v1', jsonEncode(settings.toJson()));读取时反序列化——如果 JSON 不存在,就返回一套默认配置。这里要记得处理解析失败的情况,防止上一版本写入的 JSON 结构和新版本不兼容导致崩溃。
5.2 状态同步:一个设置项改动后,怎么让所有页面感知
前面提到用 ValueNotifier 广播配置变更,这里展开说一下状态流的完整链路:
用户点击某个设置项的开关 → onChanged 触发 → SettingsController 的 update 方法创建新的 AppSettings 实例并赋给 value → 所有通过 ValueListenableBuilder 监听该 controller 的组件自动 rebuild → 打开新页面时从 controller.currentSettings 读取最新配置。
这个链路的关键点在于 AppSettings 不可变。如果你直接把整个 AppSettings 当作一个可变的单例到处修改,很快就会陷入"改了这里忘了那里"的泥潭。不可变对象 + 重建实例 + 单向数据流,是这类配置系统最简单可靠的做法。
实际开发中我用了一个小技巧:SettingsController 里提供一个 copyWith 方法,每次只改一个字段,其他字段保持原值:
class AppSettings { final ThemeMode themeMode; final bool warrantyReminderEnabled; final int reminderAdvanceDays; final String currencySymbol; AppSettings copyWith({ ThemeMode? themeMode, bool? warrantyReminderEnabled, int? reminderAdvanceDays, String? currencySymbol, }) { return AppSettings( themeMode: themeMode ?? this.themeMode, warrantyReminderEnabled: warrantyReminderEnabled ?? this.warrantyReminderEnabled, reminderAdvanceDays: reminderAdvanceDays ?? this.reminderAdvanceDays, currencySymbol: currencySymbol ?? this.currencySymbol, ); } }注意,copyWith 用 ?? 有一个小陷阱:如果你需要把一个 bool 从 true 改成 false,warrantyReminderEnabled ?? this.warrantyReminderEnabled会因为 false 也是非空值而正常工作。但如果字段是 int 或 enum,需要用 == null 判断来区分"没传"和"传了 null",写代码时要留意。
5.3 数据管理:CSV导出与清除数据的边界控制
设置页的"数据管理"分组里,我实现了导出 CSV 和清除数据两个功能。
导出 CSV 的思路比较直接:读取当前所有的家具记录列表,将每条记录的关键字段(名称、购买日期、价格、保修截止日)转成 CSV 行,然后通过文件写入插件保存到应用外部目录,并提供分享入口。这里我用了 path_provider 来获取可写目录,同样需要用到 OpenHarmony 的适配版本。保存完毕后用 SnackBar 提示用户文件路径。
清除数据这个功能要谨慎处理。我加了二次确认对话框,防止误触删除全部家具记录。对话框中明确显示"将删除全部 N 条记录,且不可恢复"。
5.4 一个容易被忽略的记录:设置变更的版本迁移
设置项的理想状态是从项目第一天就设计好存储结构。但实际开发中,我中途在 AppSettings 里新增过一个字段:reminderAdvanceDays。因为旧版本持久化的 JSON 里没有这个字段,反序列化时如果不做防御,就会得到一个 null,进而影响提醒计算的逻辑。
解决办法是在 fromJson 反序列化时对每个字段做兜底默认值处理:
factory AppSettings.fromJson(Map<String, dynamic> json) { return AppSettings( themeMode: json['themeMode'] == null ? ThemeMode.system : ..., warrantyReminderEnabled: json['warrantyReminderEnabled'] ?? true, reminderAdvanceDays: json['reminderAdvanceDays'] ?? 30, ... ); }这个处理看似简单,但对工具型 App 非常重要。存储的 JSON 结构一旦变更,要做向上兼容,否则用户升级 App 后可能出现设置丢失的严重问题。
6. 提醒设置的实战:保修期计算、本地通知与OpenHarmony兼容处理
6.1 保修期提醒的业务逻辑:不能只设一个开关
家具购买记录里最有价值的提醒,就是保修期到期提醒。这个功能的业务逻辑并不复杂:每一条家具记录都有 purchaseDate 和 warrantyMonths 两个字段,保修截止日 = purchaseDate + warrantyMonths,当前日期距离保修截止日小于等于设置的提前天数,就认为需要提醒。
我的实现是把"是否需要提醒"变成一个页面上可复用的工具函数:
bool shouldRemind(FurnitureItem item, int advanceDays) { final warrantyEnd = item.purchaseDate.add(Duration(days: item.warrantyMonths * 30)); final remaining = warrantyEnd.difference(DateTime.now()).inDays; return remaining >= 0 && remaining <= advanceDays; }这样一个函数可以在首页的家具列表卡片上直接调用,把临近过保的卡片标黄;也可以在提醒通知的触发逻辑里复用。把业务判断收敛到单一函数,比在家具列表页面里散落一堆 if 判断好维护得多。
6.2 本地通知在Flutter for OpenHarmony上的实现方案
这里要诚实地分享一个经验:本地通知插件在 OpenHarmony 上的适配并不像 shared_preferences 那么成熟。社区虽然提供了 flutter_local_notifications 的 ohos 适配,但实际跑下来有几个问题:
一是通知权限。OpenHarmony 的通知发送需要申请通知权限,并且权限的申请时机比 Android 要严格。Android 上可以在运行时请求通知权限,用户在弹窗里选择允许;OpenHarmony 上有些版本需要应用内显式触发权限申请,调用原生侧接口,而 Flutter 分支并没有把这一层完全封装好。
二是有时候通知能发出来,但通知渠道(channel)的优先级设置没有完全生效。如果你在 Android 上用过 highImportance 渠道,在 OpenHarmony 上这个优先级可能会被忽略,导致收到的通知没有声音或弹窗。
我当时采用的折中方案是:首日提醒做了两层保障——第一层,在 App 内通过页面角标和列表卡片标黄来实现可见提醒;第二层,通过 flutter_local_notifications 的 ohos 适配来发本地通知。如果插件在当前设备上跑不通,App 内的标黄机制仍然能保证核心提醒能力不丢失。
这也是跨平台开发要建立的第一个心态:不要指望一个插件在所有平台上的表现完全一致,功能降级方案要提前设计好。
7. 真机实测与避坑总结:OpenHarmony上Flutter设置的常见问题清单
7.1 插件依赖排查:怎么快速判断一个Flutter包能不能在OpenHarmony上用
做 OpenHarmony 适配,最大的时间黑洞是插件依赖。我的排查思路是三步走:
第一步,查 pubspec 插件是否有 ohos 实现。到 pub.dev 看插件详情页,或者直接在 pub-cache 里找插件的 ohos 目录。如果没有 ohos 目录,基本可以确定这个插件在 OpenHarmony 上不可用,或者需要自行桥接。
第二步,利用 dependency_overrides 指向 OpenHarmony SIG 维护的 flutter_packages 仓库。这个仓库覆盖了 shared_preferences、path_provider 等常用插件,能覆盖大部分轻量级需求。
第三步,如果某个插件确实没有适配,并且功能必要,就通过 MethodChannel 自己写一个 napi 桥接。这一步难度较高,需要同时写 Dart 端和 OpenHarmony 的 ArkTS/napi 端代码。我的建议是尽量在设计阶段就避开这类插件,比如遇到高精度定位、蓝牙通信这类强系统能力需求,在 OpenHarmony 平台上优先评估是否真的需要。
7.2 列表滚动残影与渲染引擎切换
前面我提到了 Switch 滑动残影的问题,这里再补充一个类似的:设置页中,如果某个 ListTile 包含较复杂的 trailing widget(比如组合了文字和图标),在快速滚动列表时,偶尔会出现 tile 内容闪烁或重叠。把 Impeller 切换为 Skia 后,这个问题明显缓解。
此外,OpenHarmony 的分支对 Flutter 的高刷新率支持还不算完美。在 90Hz 或 120Hz 屏幕上跑设置页列表,如果没有任何性能优化,帧率偶尔会掉到 60fps 以下。我的做法是给设置页这种轻量级页面加上 RepaintBoundary,把单个列表项的重绘隔离起来,减少不必要的布局计算。
ListView.builder( itemBuilder: (context, index) { return RepaintBoundary( child: _buildSettingsItem(context, index), ); }, )这属于常规优化手段,但在非官方支持的平台上,收益会更明显,因为底层渲染管线的调度本身就有额外开销。
7.3 文件导出与分享的路径差异
导出 CSV 后,Android 上常见的做法是把文件写到外部存储并用 FileProvider 分享。OpenHarmony 的文件系统路径布局和权限模型与 Android 不完全一致,直接用 path_provider 拿到的目录可能在应用沙箱内,用户无法直接通过文件管理器看到。
我的处理方式是把导出文件写到应用专属外部目录,同时弹出一个分享对话框,让用户通过系统分享把文件发送到微信、邮件或者文件管理应用。这样绕开了存储权限的一些限制,也符合用户实际的操作习惯。
具体实现时,我写了一个 ExportService,封装了导出流程的三个步骤:生成 CSV 内容、写入文件、启动分享。这样在设置页点击"导出数据"时,只需要调用一个方法,不用在 UI 层堆积业务逻辑。
7.4 提醒与后台运行的边界认知
最后想聊聊 OpenHarmony 上后台任务的一个现实问题。如果你了解 Android 的后台限制,OpenHarmony 在这方面的限制也类似:应用退到后台后,普通进程随时可能被回收,本地通知的定时触发并不是百分之百可靠的。
所以在这个项目里,我没有尝试做复杂的后台任务调度,而是把提醒的触发放在两个场景:一是应用启动时扫描所有记录并检查是否需要提醒;二是用户在家具列表页下拉刷新时重新计算。这种做法虽然不能做到"后台准时弹出通知",但胜在简单、可靠、不依赖复杂系统权限。
对工具类 App 来说,这种折中往往是比炫技更务实的方案。用户最需要的不是推送到达率 100%,而是打开 App 后能一眼看到哪些家具快过保了。
写在最后
这个项目做下来,我对 Flutter for OpenHarmony 的态度可以用一句话概括:能用,但别指望零成本移植。设置功能虽然只是整个家具购买记录 App 的一小块,但它把跨平台开发中最典型的几个问题全部暴露出来了——插件适配、状态同步、存储兼容、渲染引擎差异、后台能力限制。如果直接在 Android 上开发,这些问题大部分都不会遇到,但到了 OpenHarmony 上,每一个都值得花时间认真排查。
如果你也想在 OpenHarmony 上做 Flutter 项目,我的建议是从小处着手:先跑通最简单的页面,确认渲染链路正常;然后评估依赖插件,提前排除不可用的包;最后再动业务代码,并且给你的核心功能准备降级方案。
还有一个小技巧值得分享:设置功能的代码结构从一开始就要保持"配置即数据"的思想,不要在各种页面里到处写死硬编码。把所有可配置项收敛到一个模型类和一个 controller 里,后续扩展任何设置项,都只需要改动模型和设置页 UI,其他页面的适配成本会降到最低。
最后再说一个操作层面的体会:OpenHarmony 分支的版本更新节奏不如官方 Flutter 快,所以你在 GitHub 或 Gitee 上看到的一些新特性,可能移植到 OpenHarmony 上需要等待一段时间。如果你决定采用这个技术路线,要提前做好"守着稳定的旧版本"的心理准备。毕竟对工具类 App 来说,稳定运行比花哨特性重要得多。