资讯动态

Flutter适配OpenHarmony实战:家具App分类管理模块全解析

发布时间:2026/10/13 10:37:33 来源:尧图企业网站定制
最近在忙一个挺有意思的项目把一套 Flutter 写的家具购买记录 App 适配到 OpenHarmony 设备上。这个 App 本身不算复杂核心就是记录家里每件家具的购买信息比如品类、价格、购买渠道、日期这些然后按分类去统计和回顾。不过真跑起来之后我发现最花时间的反而不是业务功能本身而是“分类管理”这个听起来不起眼的模块。它牵扯到数据模型设计、持久化方案选型、UI 交互状态管理还有 OpenHarmony 上 Flutter 的种种兼容细节。今天就把这个模块的完整实现过程拆开聊聊包括我踩过的坑和最后采取的方案。这套东西适合谁看如果你正在做 Flutter 的多平台适配或者想在 OpenHarmony 上跑一个带本地存储、带增删改查的真实项目又或者只是好奇 Flutter 在非 Android 系统上到底有多“有缝”这篇都能给你一些参考。1. 方案选型Flutter 在 OpenHarmony 上的适配思路1.1 为什么要用 Flutter 写 OpenHarmony 应用选择 Flutter 来适配 OpenHarmony而不是直接用 ArkTS 重新开发主要看重两件事代码复用率和团队技术栈的延续性。我当初接这个项目的时候团队里已经有了一套为 Android 写的 Flutter 版本家具购买记录 App功能、UI、数据层都调得差不多了。要是为了适配 OpenHarmony 用 ArkTS 重新撸一遍等于把 UI、状态管理、持久化、路由全重写工作量直接翻倍后续维护还要养两套代码。Flutter 的跨端优势在于它的渲染引擎是自绘的不依赖系统原生控件。也就是说只要你把 Flutter 引擎在 OpenHarmony 上跑起来Dart 层写的 Widget 基本都能正常渲染。这跟系统 UI 组件差异导致的那种“处处要兼容”的情况完全不一样省了很大一部分适配精力。1.2 开发环境与工具链的准备在 OpenHarmony 上跑 Flutter第一步是拿到对应版本的 Flutter SDK。官方主分支的 Flutter SDK 并不直接支持 OpenHarmony你需要用 OpenHarmony 组织维护的 Flutter 分支。我当时用的版本是基于 Flutter 3.7 的 OpenHarmony 适配版。具体步骤是这样的下载 OpenHarmony 专用 Flutter SDK 分支解压到本地目录。然后在环境变量里把 flutter 命令指向这个 SDK。接着用 DevEco Studio 打开生成的 android 工程目录DevEco Studio 会自动识别并加载 OpenHarmony 的工程配置。这里有个关键点直接跑 flutter run 是行不通的OpenHarmony 的构建流程不依赖 Gradle 的 Android 插件。你需要先把 Flutter 工程编译出产物再通过 DevEco Studio 去打包和部署。我记得当时折腾环境花了差不多一整天主要卡在 SDK 版本匹配上。后来我把系统镜像、DevEco Studio、Flutter SDK 的版本都锁定到了官方文档推荐的组合才顺利跑通。这里建议不要用太新的 DevEco Studio反而老版本的兼容性更稳。2. 家具购买记录 App 的整体设计与数据模型2.1 核心业务需求拆解家具购买记录 App 的业务主线很清晰用户买了一件家具App 记录下来之后可以按分类、按时间、按价格去回顾和分析。分类管理在这个 App 里承担的角色是给所有家具记录做归类。我采用按家具类型分类的方式而不是按房间位置因为用户关注的核心维度是“我买了什么类型的家具”比如沙发、床、柜子、桌椅。分类模块的功能需求就三条新增分类、编辑分类、删除分类。不过删除分类这个操作有个业务联动分类下面可能有购买记录。如果直接把分类删了那这些购买记录就变成“无家可归”的状态。我最后的处理方式是删除分类前检查该分类下是否有记录有的话提示用户并让用户决定是否连带删除记录。2.2 本地数据存储方案选择Flutter 在 Android 上常用的本地数据库是 sqflite但在 OpenHarmony 上直接跑 sqflite 会碰到原生插件适配的问题。因为 sqflite 依赖 Android 的 SQLite APIOpenHarmony 虽然也有 SQLite但接口路径、JNI 绑定方式都不一样官方适配插件当时还不稳定。我试过两种替代方案用 drift 数据库框架的 OpenHarmony 适配版以及直接用 JSON 文件存本地。drift 适配版当时在真机上读写有点慢而且生成代码的构建步骤和 OpenHarmony 的打包流程有冲突最后还是放弃了。我最后采用的是 JSON 文件存储方案。每条分类就是一个 JSON 对象整个分类列表存成一个 JSON 数组写入应用私有目录下的文件。考虑到分类管理的数据量非常小一般家用场景可能也就几十个分类JSON 文件完全扛得住而且实现简单、可控性强。2.3 分类数据模型设计分类模型在设计时考虑了家具场景的特殊需求除了基础字段还加了一个图标字段用于 UI 展示。我用的是内置图标库的索引值而不是存图片这样可以避免引入资源文件管理和网络加载的问题。实际模型定义如下class Category { String id; // 唯一标识用时间戳 随机数生成 String name; // 分类名称如沙发、床、柜子 int iconIndex; // 图标索引对应内置图标列表 double budget; // 分类预算可选字段用于后续统计 int createdAt; // 创建时间戳用于排序 }budget 字段是后来加的。用户给“沙发”设一个预算上限然后购买记录表里每添加一条沙发记录App 就能自动算这个分类的剩余预算。这个功能很实用也是我推荐你在做类似项目时一定要预留的字段因为买家具这件事本来就有预算规划的需求。主键 id 不用自增数字而是用 DateTime.now().millisecondsSinceEpoch 加上一个随机数拼接。这样在同步数据或者未来做多端数据合并时不会因为自增 ID 冲突导致数据覆盖。2.4 购买记录与分类的关联设计分类和购买记录是典型的一对多关系。我在购买记录模型里存了 categoryId 字段而不是嵌套存整个分类对象。这样做的原因是如果分类名称改了所有购买记录里显示的名称会自动跟着变如果嵌套存储就要遍历所有记录去更新很容易漏。有人可能会问那如果分类删了呢前面说过删除分类时会检查并处理关联记录。具体逻辑是在执行删除之前先查一下该分类下的记录数量如果大于零就弹出对话框提示用户并提供两个选项“连记录一起删”和“取消删除”。这个设计虽然简单但避免了最头疼的脏数据问题。3. 分类管理核心功能实现3.1 服务层分类数据的增删改查不管 UI 怎么变数据层先稳定下来。我单独建了一个 CategoryService把所有操作封装成异步方法这样后续切换到真正的数据库也不用改 UI 代码。class CategoryService { static final String _filePath /data/storage/el2/base/files/categories.json; static FutureListCategory loadCategories() async { File file File(_filePath); if (!await file.exists()) return []; String jsonStr await file.readAsString(); Listdynamic list json.decode(jsonStr); return list.map((e) Category.fromJson(e)).toList(); } static Futurevoid saveCategories(ListCategory categories) async { File file File(_filePath); String jsonStr json.encode(categories.map((e) e.toJson()).toList()); await file.writeAsString(jsonStr); } static Futurevoid addCategory(Category category) async { ListCategory categories await loadCategories(); categories.add(category); await saveCategories(categories); } static Futurevoid updateCategory(Category category) async { ListCategory categories await loadCategories(); int index categories.indexWhere((e) e.id category.id); if (index 0) { categories[index] category; await saveCategories(categories); } } }这里是按 OpenHarmony 的实际应用沙箱路径写的文件路径。Android 上你一般会通过 path_provider 拿路径但 path_provider 这个插件在 OpenHarmony 上的适配我当时测试还不稳定干脆直接写了。如果你对路径不确定可以在真机上跑一段代码打印 Directory.current.path 看看当前目录在哪。我调试的时候发现 OpenHarmony 的沙箱路径和 Android 差别不小别按 Android 的目录结构去推测。3.2 数据源隔离在 UI 层和文件层之间加缓冲直接每个页面都调用 loadCategories 从文件读性能上没问题但会导致 UI 状态难以统一。比如分类列表页里删了一个分类返回首页统计页面时首页的分类数据还是旧的必须重新触发一次加载。我的方案是引入了一个单例的 CategoryStore内存中维护一份 ListCategory所有页面通过它读写每次写操作成功后同时更新内存和文件。class CategoryStore { static final CategoryStore instance CategoryStore._(); CategoryStore._(); ListCategory _categories []; ListCategory get categories List.unmodifiable(_categories); Futurevoid init() async { _categories await CategoryService.loadCategories(); } Futurevoid add(Category category) async { _categories.add(category); await CategoryService.saveCategories(_categories); } Futurevoid delete(String id) async { _categories.removeWhere((e) e.id id); await CategoryService.saveCategories(_categories); } }这样做最大的好处是状态同步及时。任何一个页面操作完分类其他页面通过 Store 拿到的都是最新数据配合 Provider 做界面刷新非常顺滑。3.3 分类列表页的实现分类列表页是整个模块的主页面。我把它分成两部分上半部分是分类统计小结下半部分是分类列表完整代码实现如下class CategoryListPage extends StatelessWidget { override Widget build(BuildContext context) { final categories CategoryStore.instance.categories; int totalCount categories.length; return Scaffold( appBar: AppBar(title: Text(分类管理)), body: Column( children: [ Padding( padding: EdgeInsets.all(16), child: Text(共 $totalCount 个分类), ), Expanded( child: ListView.separated( itemCount: categories.length, itemBuilder: (context, index) { final item categories[index]; return ListTile( leading: CircleAvatar( child: Icon(IconData(item.iconIndex)), ), title: Text(item.name), subtitle: Text(预算${item.budget} 元), trailing: PopupMenuButton( onSelected: (value) { if (value edit) { // 跳转编辑页 } else if (value delete) { // 删除操作 } }, itemBuilder: (_) [ PopupMenuItem(value: edit, child: Text(编辑)), PopupMenuItem(value: delete, child: Text(删除)), ], ), onTap: () { // 跳转到分类详情 }, ); }, separatorBuilder: (_, __) Divider(height: 1), ), ), ], ), ); } }列表用 ListView.separated 来渲染分类卡片里展示了分类名称、预算和图标。PopupMenuButton 是 Flutter 内置的菜单组件在 OpenHarmony 上渲染为右上角的三个点按钮点击弹出编辑和删除选项。使用 PopupMenuButton 而不是常用的长按弹出菜单主要考虑是给用户更直观的“可见可点”操作方式特别是不太熟悉长按手势的普通用户。3.4 新增与编辑分类页实现新增分类和编辑分类共用同一个页面通过传入的分类参数决定模式。核心表单字段包括分类名称输入框、图标选择器、预算输入框。class CategoryEditPage extends StatefulWidget { final Category? category; override StateCategoryEditPage createState() _CategoryEditPageState(); } class _CategoryEditPageState extends StateCategoryEditPage { late final TextEditingController _nameController; late final TextEditingController _budgetController; int _selectedIconIndex 0; bool get isEdit widget.category ! null; override void initState() { super.initState(); _nameController TextEditingController(text: widget.category?.name ?? ); _budgetController TextEditingController( text: widget.category?.budget.toString() ?? ); _selectedIconIndex widget.category?.iconIndex ?? 0; } Widget _buildIconPicker() { ListIconData icons [ Icons.chair, Icons.weekend, Icons.bed, Icons.cabinet, Icons.table_restaurant, Icons.light, ]; return GridView.count( crossAxisCount: 6, shrinkWrap: true, physics: NeverScrollableScrollPhysics(), children: icons.map((icon) { int index icons.indexOf(icon); return GestureDetector( onTap: () setState(() _selectedIconIndex index), child: Container( decoration: BoxDecoration( shape: BoxShape.circle, color: _selectedIconIndex index ? Colors.blue.withOpacity(0.2) : Colors.transparent, ), child: Icon(icon), ), ); }).toList(), ); } }图标选择器用 GridView 配合六个家具相关图标做展示选中后高亮背景色。为什么用 index 而不是直接用 IconData因为图标是存在 JSON 里的JSON 里不能存 IconData 对象存索引就方便了——存的时候存数字读的时候通过索引在图标列表里反查。保存逻辑是这样Futurevoid _save() async { String name _nameController.text.trim(); if (name.isEmpty) { ScaffoldMessenger.of(context).showSnackBar( SnackBar(content: Text(分类名称不能为空)) ); return; } double budget double.tryParse(_budgetController.text) ?? 0; final category Category( id: isEdit ? widget.category!.id : DateTime.now().millisecondsSinceEpoch.toString(), name: name, iconIndex: _selectedIconIndex, budget: budget, createdAt: isEdit ? widget.category!.createdAt : DateTime.now().millisecondsSinceEpoch, ); if (isEdit) { await CategoryStore.instance.update(category); } else { await CategoryStore.instance.add(category); } Navigator.pop(context); }这里保存前只做了非空校验预算字段允许为空空的时候按 0 处理。因为预算在分类管理里不是必填项用户可以不设后续统计时不计算超预算逻辑就行了。3.5 删除分类与级联记录处理删除分类的完整逻辑是实现上最需要小心的地方。服务层准备好之后UI 层的调用要加一层确认和保护。Futurevoid _deleteCategory(Category category) async { int recordCount await RecordService.countByCategory(category.id); if (recordCount 0) { // 直接删除 await CategoryStore.instance.delete(category.id); return; } bool? confirmed await showDialogbool( context: context, builder: (context) AlertDialog( title: Text(删除分类), content: Text(该分类下有 $recordCount 条购买记录是否连同记录一起删除), actions: [ TextButton( onPressed: () Navigator.pop(context, false), child: Text(取消), ), TextButton( onPressed: () Navigator.pop(context, true), child: Text(连记录一起删), ), ], ), ); if (confirmed true) { await RecordService.deleteByCategory(category.id); await CategoryStore.instance.delete(category.id); } }在删除前先查询关联记录数量这条查询语句是 RecordService 里最简单的按条件计数。如果记录为零静默删除如果有记录必须让用户知晓。这个确认步骤虽然多一次弹窗但避免了很多“事后找不回来”的麻烦。我踩过一个很典型的坑开始没做这个检查直接删了一个分类。后来用户反馈之前记的“餐桌”购买记录全没了只剩一张孤零零的收据。从此以后任何删除操作我都遵循“先查关联再确认再删除”的三步原则。4. OpenHarmony 适配问题与排查记录4.1 文件路径与沙箱机制前面提到过OpenHarmony 的文件路径和 Android 不一样。Android 的 app 私有目录通常通过 path_provider 获取路径类似 /data/data/包名/files。OpenHarmony 的应用沙箱路径结构更复杂并且不同 API 版本的路径规则有差异。我建议的做法是第一先跑一个测试页面用 Directory.current 打印当前路径第二把文件读写封装成单独的服务层路径集中在一个常量文件里第三不要用绝对路径写死而是通过相对路径结合当前目录拼接。如果你用的是较新版本的 OpenHarmony可以尝试通过 device.getApplicationInfo 相关 API 获取沙箱路径。老版本就直接写死 /data/storage/el2/base/files亲测可用。4.2 Flutter 插件的兼容性边界Flutter 生态里很多常用插件在 OpenHarmony 上都是不可用的这是一个必须提前接受的事实。我遇到的具体问题包括path_provider 不可用、shared_preferences 有适配版但需要单独引入、sqflite 完全不可用。最好的策略是尽量减少插件依赖能用 Dart 原生能力解决的就不引插件。比如 JSON 存储我就不用任何插件Directly 用 dart:io 的 File 读写。Dart 的 isolate、异步 IO、字符串处理完全够用。如果你要加图片、网络、本地通知这些功能要先查一下对应的 OpenHarmony 适配插件在不在维护。有些以社区贡献者维护形式存在质量参差不齐尽量以官方示例项目里的推荐插件为准。4.3 构建产物与签名配置OpenHarmony 的 Flutter 工程构建流程和 Android 差别非常大。Android 跑 flutter build apk 就得到一个 apk签好名就能装。OpenHarmony 这边需要先生成 HAP 包然后配签名。签名配置要提前做。我当时拿到的开发设备要求必须先有签名证书才能安装应用没有签名的 HAP 包直接装不上。配置签名的时候注意需要把对应模块的 profile 文件和证书文件都放在工程目录里并在 DevEco Studio 的界面里完成配置。还有一个小坑每次丢签名配置或者载入新工程时构建出来的 HAP 包可能还是旧签名。清洁构建一下把 build 目录删掉再重新生成。4.4 中英文输入法与键盘问题在 OpenHarmony 真机上Flutter 自带的文本输入能力存在一个已知问题系统中文输入法弹出的键盘在部分版本上无法正确关联 Flutter 的输入框。具体表现是你点击输入框键盘弹出来了但按键没有反应或者输入的字母不进入文本框。这个问题一开始让我以为是代码写错了排查了很久。后来发现是 Flutter 引擎和系统的输入法服务对接不完整。临时解决方案是在输入框上强行设置某个输入类型部分情况下能触发正确的输入关联。如果遇到这个问题可以试试在 TextField 上设置 keyboardType 和 textInputAction尽量避免使用明文类型multiline或者升级到修复过该问题的 Flutter 适配版本。这个问题已经有人在 GitHub 上反馈新的适配分支有修复但需要重新集成。4.5 调试效率提升技巧OpenHarmony 上的 Flutter 调试热重载Hot Reload支持的稳定性不如 Android。有一次我在真机上频繁热重载直接导致设备和开发机连接断开必须重新拔插 USB。后来我的工作流是先写好单元测试保证业务逻辑无误再部署到真机做 UI 验证。UI 验证时尽可能批量调整减少热重载次数。真机和模拟器的渲染效果也有差异模拟器流畅不代表真机流畅最终以真机为准。5. 分类管理的经验总结与后续优化方向5.1 当前实现方案的总结性评估这套分类管理模块跑下来整体稳定性没问题。JSON 文件方案在当前数据量下读写速度尚可分类列表页几百条记录滑动流畅没有卡顿。内存单例的 CategoryStore 保证各页面数据一致增删改查逻辑清晰代码可维护性比较高。不过要清楚这个方案的上限。当购买记录数量达到几千条、分类数量超过一百时每次改动都重写整个 JSON 文件的策略就不合适了。这时候需要引入真正的数据库方案或者至少把文件切分成“分类表文件”和“记录表文件”分别维护。我现在还能用 JSON是因为家具购买记录这种场景天然低频一天能记录三四条就不错了。5.2 下一步可以加的实用功能基于现在这个分类管理基础有几个扩展方向是很自然的分类排序功能。现在的列表按创建时间排列用户新建一个“其他”分类后只能排到最后体验一般。可以加一个拖拽排序把排序结果存进 Category 模型的 sortOrder 字段展示时按 sortOrder 排序。分类预算超限提醒。模型里已经有 budget 字段下一步可以做统计分类下所有记录的总金额超过预算时在列表页显示红色警示。这个功能需要在 RecordService 里加一个按分类分组求金额的聚合方法不算复杂。分类合并功能。用户很可能创建了“沙发”和“沙发椅”两个分类实际买完发现在统计时想合并。做这个功能需要批量更新购买记录的 categoryId把旧分类的记录全部转移到新分类下再删掉旧分类。这个功能对数据一致性要求高值得仔细设计。5.3 我个人在实际适配过程中的体会这几个星期的适配经历让我最深刻的一点体会是所谓跨平台适配最大的成本不在 UI 渲染而在生态依赖。Flutter 在 OpenHarmony 上跑 UI 没问题但周边插件的适配进度直接影响项目的技术选型和实施排期。如果你要做的 App 大量依赖地图、支付、推送这类第三方原生能力那在 OpenHarmony 上的适配成本会呈指数上升。反过来像我们这种数据逻辑为主、UI 自绘、不依赖系统独有能力的工具类 App适配成本就很可控。另一个体会是项目架构的低耦合设计在跨端适配时价值特别大。因为数据层封装得干净切换存储方案时我只动了 CategoryService 内部的实现UI 层完全不用改。如果当初把数据库操作散落在各个页面里这次适配光是改路径和改读写逻辑就够崩溃的。最后如果你也计划做 OpenHarmony 的 Flutter 适配建议先做一个小规模的验证工程只包含一个页面、一个文件读写操作完整走通“开发-编译-签名-真机部署”链路之后再开始迁移业务代码。这个验证工程能帮你提前暴露工具链的问题避免在业务代码上边适配边排查效率完全不一样。

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑