从去年开始,我陆续接到好几个朋友的需求,都是同一个意思:自己用Flutter写的生活助手类App,想尽快跑上基于OpenHarmony的设备。说实话,一开始我对这个组合是持保留态度的,毕竟Flutter的OpenHarmony分支在去年还属于“能编译但不敢上生产”的阶段。但架不住需求催得紧,我花了一个周末把环境搭起来,又把一个生活助手App里最核心的“分类管理”功能完整实现了一遍。跑通之后我最大的感受是:这条路真的能走,而且比想象中顺畅,但前提是你得把几处关键的设计决策做对。
这篇文章就是我这次实战的完整记录,从OpenHarmony和HarmonyOS NEXT的关系、Flutter在鸿蒙生态的适配现状,到环境搭建、数据层设计、界面实现、原生能力打通,再到我踩过的坑,一次讲清楚。如果你正准备用Flutter开发或移植一个OpenHarmony应用,尤其是那种带数据管理、系统交互的应用,这篇文章应该能帮你省掉不少试探时间。
1. 为什么要在OpenHarmony上赌一把Flutter:适配现状与选型思考
1.1 先搞清楚OpenHarmony和HarmonyOS NEXT的关系
很多刚接触这个方向的开发者,第一个问题就是:OpenHarmony和手机上的HarmonyOS到底是不是同一个东西?这里我直接用大白话解释。
OpenHarmony是一个开源操作系统底座,属于底层能力开源项目,普通用户接触不到。而HarmonyOS NEXT是基于OpenHarmony的一款商业操作系统。两者关系有点像开源Linux内核和发行版的关系。
这个区别对于Flutter开发者的意义在于:Flutter官方适配的是OpenHarmony的API,而不是某个厂商的私有SDK。所以只要你的App跑在OpenHarmony兼容的设备上,理论上在各家的HarmonyOS NEXT设备上都能运行。这也是我觉得值得投入的根本原因——你写的Flutter代码,编译后的产物不再依赖Android的ART运行时,而是运行在OpenHarmony的应用框架上,这是一条独立于Android生态的路线。
1.2 现阶段的适配程度:能做什么,不能做什么
我实测下来,OpenHarmony分支的Flutter引擎,UI渲染、事件分发、路由管理这些核心能力都已经能正常使用。官方仓库flutter_flutter的ohos分支目前维护到了Flutter 3.x的版本线,我在项目里用的是3.7.12-ohos版本,Dart SDK对应2.19.x,日常的Widget开发体验和标准Flutter几乎一致。
但要注意几个边界。
第一,不是所有pub.dev上的Flutter插件都能直接用。像shared_preferences、path_provider这种纯Dart实现的,或者依赖原生能力较浅的插件,适配起来很快。但依赖Android/iOS原生SDK的插件就要逐个排查,很多都需要等社区适配或自己写平台通道。
第二,PlatformView支持是存在的,flutter_ohos包提供了平台视图的能力,但在OpenHarmony上的渲染性能相比Android还有差距。我的建议是:能用普通Widget实现的交互,就不要引入原生View。
第三,热重载(Hot Reload)在真机上偶尔会失灵,尤其是改到原生侧代码或平台通道相关的文件时,重启比等热重载更快。
所以我的选型结论是:核心业务逻辑用纯Flutter实现,凡是需要系统能力的地方,比如读相册、拿系统信息,通过MethodChannel和EventChannel自己封装一层原生适配。这样一来,即便某个第三方插件没跟上OpenHarmony,我们也不至于被卡死在依赖上。
2. 环境搭建的完整链路:从SDK到真机部署
2.1 版本搭配:这步最容易绕晕,建议直接抄作业
Flutter for OpenHarmony的环境搭建,最大的坑就是版本匹配。我一开始按照网上很老的文章装了一版,结果连flutter doctor都跑不通。后来总结出一套可以稳定工作的组合,直接列在下面。
| 组件 | 版本/说明 |
|---|---|
| Flutter SDK | 使用OpenHarmony官方维护的flutter_flutter仓库,切换ohos分支,版本3.7.12-ohos |
| Dart SDK | 随Flutter SDK自带,无需单独安装 |
| Node.js | 14.19.1及以上,编译工具链需要 |
| hdc工具 | 随DevEco Studio安装,用于真机部署 |
| DevEco Studio | 4.0及以上版本,用于打开ohos工程做原生侧配置 |
| OpenHarmony设备 | 推荐用HarmonyOS NEXT测试机或RK3568开发板 |
环境变量上的配置,除了标准的Flutter相关变量,最关键的是要加上OpenHarmony的SDK路径。我在.bashrc里这样配:
export DEVECO_SDK_HOME=/opt/DevEcoStudio/sdk export PATH=$PATH:/opt/DevEcoStudio/tools/ohpm/bin这里有个很容易踩的坑:很多教程会让你直接配OHOS_SDK_HOME,但不同版本的DevEco Studio对变量名的要求不一样。如果你用的是DevEco Studio 4.0以上的版本,务必认准DEVECO_SDK_HOME,否则编译时找不到native工具链。
2.2 从创建工程到真机部署的完整流程
环境变量配好之后,创建工程的命令和标准Flutter几乎一样,唯一多了个--platforms参数:
flutter create --platforms ohos my_assistant_app执行完后,项目里会多出一个ohos目录,这就是OpenHarmony的工程壳,类似Android项目里的android目录。第一次跑的时候,需要用DevEco Studio打开这个ohos目录,让IDE自动下载和配置鸿蒙侧的依赖。
接下来连接真机。用USB线连上开发机后,先执行hdc list targets确认设备能被识别:
hdc list targets如果列表为空,检查一下设备是否开启了USB调试模式,以及hdc命令路径是否在PATH里。设备识别之后,直接运行:
flutter run -d <device-id>第一次编译会很久,因为要同时构建Dart AOT产物和OpenHarmony应用包,建议给自己留半小时的耐心。编译完成后,App会直接安装到设备上并启动。
有个关于编译链的细节值得说:OpenHarmony工程默认用ohpm管理原生依赖,ohpm install如果遇到网络问题,可以换镜像源,但我不展开讲这个,毕竟不同网络环境下的最优选择差异太大,大家按各自实际情况处理。我的经验是,只要flutter create成功,ohpm install基本不会出大问题,出问题的往往是SDK路径配错。
3. 分类功能的建模与存储:生活助手数据层的设计
3.1 生活助手里“分类”到底管哪些事
分类管理看着简单,但凡是做过实际项目的人都知道,这个模块牵扯的东西一点都不少。在我这个生活助手里,分类被用在三个场景:
- 记账:支出类别(餐饮、交通、购物、医疗等)和收入类别(工资、理财、兼职等)。
- 待办事项:工作、学习、家庭、健康等任务分组。
- 物品管理:比如家里的杂物、工具、药品,按存放位置或类型归类。
所以我在设计分类模型时,刻意没有把分类和某个具体业务场景绑定死,而是定义成一个通用的实体。每个分类包含下面这些字段:
| 字段 | 类型 | 说明 |
|---|---|---|
| id | String | 主键,用UUID |
| name | String | 分类名称 |
| icon | String | 图标,存路径或Emoji字符 |
| colorValue | int | 分类卡片背景色 |
| sortOrder | int | 排序权重,越小越靠前 |
| isDefault | bool | 是不是系统预置分类 |
| isArchived | bool | 是否已归档,软删除标识 |
| createdAt | int | 创建时间戳 |
这套设计的好处是:后续不管是做统计报表还是做多场景复用,都不需要大改数据结构。
3.2 存储方案选型:为什么我没有一上来就用sqflite
存储方案上,我做了好几轮对比。
第一个想到的是sqflite,但查了一圈发现,在OpenHarmony上使用sqflite需要适配,通常要依赖社区移植的sqlite3原生库,编译链比较曲折,而且升级Flutter版本后可能又要重新适配。对于分类管理这种体量的数据,引入数据库属于杀鸡用牛刀。
第二个是drift,虽然Dart侧很优雅,但它底层同样依赖sqlite3,在OpenHarmony上的适配情况跟sqflite一样存在风险。
第三个是shared_preferences,这个插件已经有OpenHarmony适配版本,而且在纯Dart层可以正常工作。分类数据量一般就几十条,每次操作全量重写一份JSON,性能完全够用。
我用了一张表把这些方案的关键差异整理出来,方便你对照:
| 方案 | OpenHarmony适配成本 | 数据量承受能力 | 事务/查询能力 | 我的结论 |
|---|---|---|---|---|
| shared_preferences | 低,有现成适配 | 几百条内无压力 | 弱 | 初期首选,快速跑通业务 |
| sqflite | 中,需要适配编译 | 大 | 强 | 数据量大后再迁移 |
| drift | 中高 | 大 | 强 | 比sqflite更优雅,但依赖更重 |
| 自己用平台通道封装文件读写 | 中 | 中 | 弱 | 不推荐,轮子重复 |
最终我的选择是:先用shared_preferences存JSON,但在仓储层做一个清晰的接口抽象,将来要迁到sqlite,只需要替换仓储实现,上层UI完全不用动。
3.3 模型与仓储层的Dart实现
模型类写得比较直白,重点是序列化和反序列化要稳定,因为整个存储都依赖JSON:
class Category { final String id; final String name; final String icon; final int colorValue; final int sortOrder; final bool isDefault; final bool isArchived; final int createdAt; const Category({ required this.id, required this.name, required this.icon, required this.colorValue, required this.sortOrder, required this.isDefault, required this.isArchived, required this.createdAt, }); Map<String, dynamic> toJson() => { 'id': id, 'name': name, 'icon': icon, 'colorValue': colorValue, 'sortOrder': sortOrder, 'isDefault': isDefault, 'isArchived': isArchived, 'createdAt': createdAt, }; factory Category.fromJson(Map<String, dynamic> json) => Category( id: json['id'] as String, name: json['name'] as String, icon: json['icon'] as String, colorValue: json['colorValue'] as int, sortOrder: json['sortOrder'] as int, isDefault: json['isDefault'] as bool, isArchived: json['isArchived'] as bool, createdAt: json['createdAt'] as int, ); Category copyWith({String? name, String? icon, int? colorValue, int? sortOrder, bool? isArchived}) { return Category( id: id, name: name ?? this.name, icon: icon ?? this.icon, colorValue: colorValue ?? this.colorValue, sortOrder: sortOrder ?? this.sortOrder, isDefault: isDefault, isArchived: isArchived ?? this.isArchived, createdAt: createdAt, ); } }仓储层我用了单例模式,所有分类数据的读写都走这同一个入口:
class CategoryRepository { static const _storageKey = 'categories'; static final CategoryRepository _instance = CategoryRepository._(); factory CategoryRepository() => _instance; CategoryRepository._(); Future<List<Category>> loadAll() async { final prefs = await SharedPreferences.getInstance(); final raw = prefs.getString(_storageKey); if (raw == null || raw.isEmpty) return []; final list = jsonDecode(raw) as List<dynamic>; return list .map((e) => Category.fromJson(e as Map<String, dynamic>)) .toList() ..sort((a, b) => a.sortOrder.compareTo(b.sortOrder)); } Future<void> saveAll(List<Category> categories) async { final prefs = await SharedPreferences.getInstance(); final raw = jsonEncode(categories.map((e) => e.toJson()).toList()); await prefs.setString(_storageKey, raw); } Future<void> insert(Category category) async { final all = await loadAll(); all.add(category); await saveAll(all); } Future<void> update(Category category) async { final all = await loadAll(); final index = all.indexWhere((e) => e.id == category.id); if (index >= 0) { all[index] = category; await saveAll(all); } } Future<void> deleteById(String id) async { final all = await loadAll(); all.removeWhere((e) => e.id == id); await saveAll(all); } }这个仓储层有几个容易被忽略的设计点。第一是saveAll每次全量写回,虽然“浪费”,但在数据量小的情况下反而简化了逻辑,不需要考虑增量同步。第二是排序统一在loadAll里做,调用方永远拿到有序列表。第三是软删除字段isArchived已经放进模型里了,但业务上我还不急着用,这个后面讲删除的时候会细说。
4. 分类管理界面的实现:列表、表单与导航状态保持
4.1 主界面布局:左侧分类树,右侧条目列表
生活助手的分类管理界面,我最终做成了左右双栏结构。左侧是分类的树状列表,支持一级分类和二级分类;右侧展示当前分类下的具体条目,比如这个分类里的记账明细、待办事项或物品列表。
这个布局在手机上会有点挤,但对于OpenHarmony的目标设备(平板、折叠屏、开发板)来说非常合适。拆解下来,主界面的核心代码结构大致是这样:
Widget build(BuildContext context) { return Scaffold( appBar: AppBar( title: const Text('分类管理'), actions: [ IconButton( icon: const Icon(Icons.add), onPressed: _showAddCategorySheet, ), ], ), body: Row( children: [ SizedBox(width: 180, child: _buildCategoryTree()), VerticalDivider(width: 1), Expanded(child: _buildItemList()), ], ), ); }左侧分类树的每一项用ListTile展示图标和名称,选中态用分类自身的colorValue做背景色高亮。右侧条目列表用RefreshIndicator包了ListView.separated,支持下拉刷新,条目卡片上会显示分类来源、时间、关键信息。
这里有个经验之谈:分类树和条目列表之间不要做得太耦合。我一开始把“当前选中分类”直接存在StatefulWidget的state里,右列表每次都要依赖左列表的状态重建,后来发现一旦页面多起来,这种隐式依赖非常难维护。改造后我用了Provider管理selectedCategoryId,左列表和右列表各自消费同一个状态源,界面再复杂也不会乱。
4.2 新增与编辑:表单交互的完整闭环
新增分类的入口,我用了底部弹出面板(showModalBottomSheet),里面是一张完整的表单:
- 分类名称:必填,限制10个字符以内,重名校验。
- 图标:默认给一组Emoji和Material图标供选择,也支持从系统图库选图片。
- 颜色:预设8种色板,点击切换。
- 排序:一个滑动条,控制分类在列表里的位置。
- 是否设为默认分类:开关项,开启后新条目默认归入这个分类。
表单校验我放在了Form+TextFormField体系里,提交时统一校验,有错误就聚焦到第一个错误字段。保存时生成一个UUID作为分类id,createdAt取当前毫秒时间戳,然后调用仓储层的insert方法。
编辑的流程基本是复用一个表单数据模型,只是打开面板时把已有分类的数据填充进去。这里我特意没有让更新操作直接改原对象引用,而是用copyWith生成新对象再入库,避免列表页因为引用相等而不刷新。
4.3 Navigator切换后状态不会丢,但要你自己动手
在开发过程中,我注意到一个很实际的问题,也有朋友专门问过我:“Flutter里用Navigator切到别的页面,再切回来,分类列表的状态还在不在?”这个问题其实没有标准答案,完全取决于你页面是怎么实现的。
如果你用的是Navigator.push跳转到一个新路由,那原路由的State会被完整保留在导航栈里,切回来时列表滚动位置、选中状态都还在。但如果你用的是IndexedStack或者PageView做底部Tab切换,默认情况下所有子页面都会保持状态,因为它们在视图树里一直没有被销毁。
真正会丢状态的是下面这两种场景:一是用Navigator.pushReplacement替换了当前路由,原来的State会被销毁;二是页面在TabBarView里,滑动远离后又滑回来,如果没做保活处理,State可能会被回收。
保住状态的方案,我用的是AutomaticKeepAliveClientMixin:
class CategoryListPage extends StatefulWidget { const CategoryListPage({super.key}); @override State<CategoryListPage> createState() => _CategoryListPageState(); } class _CategoryListPageState extends State<CategoryListPage> with AutomaticKeepAliveClientMixin { @override bool get wantKeepAlive => true; @override Widget build(BuildContext context) { super.build(context); return /* 列表内容 */; } }注意一定要在build方法第一行调用super.build(context),这个是很多人漏掉的,漏了之后KeepAlive完全不生效,而且不报错,属于那种排查半天才发现的问题。
5. 原生能力打通:EventChannel与MethodChannel在分类管理中的实战介入
5.1 分类图标选择:为什么不能只靠Flutter层
分类管理里最需要原生能力的地方,就是图标选择。Emoji和内置Material图标虽然够用,但用户想用自己的照片做分类封面时,就必须调用系统相册。而Flutter本身是不带相册访问能力的,必须走平台通道。
我选择的原生能力接入方式是一个很标准的组合套路,也是Flutter对OpenHarmony适配后该怎么用的正确示例:
- 需要一次性的数据获取,比如选图片、读系统信息,用
MethodChannel。 - 需要持续监听系统侧事件,比如相册内容变化、系统电量变化,用
EventChannel。
这两个通道的注册代码,写在一个专门的ChannelManager类里统一管理。它在OpenHarmony侧的实现,需要放到ohos/entry/src/main/ets目录下的Ability相关文件里,通过系统提供的API和Flutter引擎通信。
5.2 MethodChannel实现从系统相册选图
Flutter侧的调用端,我封装了一个静态方法,方便界面层直接调用:
class GalleryPicker { static const MethodChannel _channel = MethodChannel('com.example.assistant/gallery'); static Future<Uint8List?> pickImage() async { try { final result = await _channel.invokeMethod<Uint8List>('pickImage'); return result; } on PlatformException catch (e) { debugPrint('选择图片失败: ${e.message}'); return null; } } }这里有一个值得注意的技术细节:相册图片通过二进制字节流返回,而不是返回一个文件路径。因为OpenHarmony应用的沙箱路径和Android不完全一样,直接传路径很容易出现权限问题或路径失效,传二进制反而最稳。代价是内存占用——一张几MB的照片会以字节数组形式在原生和Dart之间拷贝一次,所以调用完要尽快把字节转成Image对象并释放引用。
原生侧实现的核心思路是:打开系统相册选择器,拿到图片的URI后,读取其字节流,通过result.success(byteArray)回传给Flutter。注意注册通道时机要在Ability的onWindowStageCreate生命周期里完成,太早会导致通道还没建立就被调用。
5.3 EventChannel监听系统相册变化
选了图片之后,还有一个隐藏的需求:当用户在系统相册里新增或删除了照片,分类封面最好能自动更新。这就是典型的EventChannel场景。
Flutter侧的监听代码:
class PhotoEventBus { static const EventChannel _eventChannel = EventChannel('com.example.assistant/photo_events'); static void startListening() { _eventChannel.receiveBroadcastStream().listen((event) { final type = event as String; if (type == 'photo_added') { // 刷新当前分类的图标缓存 } else if (type == 'photo_removed') { // 提示用户图标可能已失效 } }); } }EventChannel和MethodChannel在使用体验上的最大区别是:EventChannel是持续性的,一旦开始监听,只要Flutter侧没有取消订阅,事件就会一直推送过来。所以务必要在合适的时机调用cancel,否则页面销毁后回调还在执行,轻则内存泄漏,重则引发UI在不可见页面上的异常更新。
我在分类管理的具体实现里,EventChannel派上的另一个用处是监听设备电量,当电量低于20%时,界面上的分类图标色块会变灰,算是一个实用又不过度设计的小功能。
6. 增删改查的完整闭环:从表单校验到级联处理
6.1 新增分类的校验与落库流程
新增分类是整个模块的入口,也是最容易出数据问题的环节。我把新增流程分成四步:
第一步,表单校验。名称不能为空、不能超过10个字、不能有重复。这里有一个简单的重复检查逻辑:
Future<bool> _isNameDuplicate(String name) async { final all = await CategoryRepository().loadAll(); return all.any((c) => c.name == name && !c.isArchived); }第二步,构造Category对象。注意sortOrder的默认值为当前最大排序加1:
final all = await CategoryRepository().loadAll(); final maxOrder = all.isEmpty ? 0 : all.map((e) => e.sortOrder).reduce((a, b) => a > b ? a : b); final newCategory = Category( id: uuid.v4(), name: _nameController.text.trim(), icon: _selectedIcon, colorValue: _selectedColor, sortOrder: maxOrder + 1, isDefault: _isDefault, isArchived: false, createdAt: DateTime.now().millisecondsSinceEpoch, );第三步,调用仓储层insert落库。第四步,刷新界面状态,更新分类树和条目列表。
6.2 编辑与排序:保持数据一致性的关键细节
编辑分类时,最容易出的问题不是改名字,而是改了颜色、图标之后,所有引用这个分类的条目卡片不同步。我的处理方式是统一走ChangeNotifier通知:仓储层更新完成后,触发一次State刷新,所有监听该状态的Widget自动重建。
操作步骤本身不复杂:
final updated = _currentCategory.copyWith( name: _nameController.text.trim(), icon: _selectedIcon, colorValue: _selectedColor, sortOrder: _selectedOrder, ); await CategoryRepository().update(updated);排序调整用了两种方式:一是编辑表单里的滑动条,二是列表页的长按拖拽。拖拽排序我用了ReorderableListView,在onReorder回调里重排列表并重新计算sortOrder:
void _onReorder(int oldIndex, int newIndex) { setState(() { if (newIndex > oldIndex) newIndex -= 1; final item = _categories.removeAt(oldIndex); _categories.insert(newIndex, item); for (var i = 0; i < _categories.length; i++) { _categories[i] = _categories[i].copyWith(sortOrder: i + 1); } }); _saveAfterReorder(); }这里有个React类框架里很常见的坑:直接修改_categories里的对象字段不会触发UI更新,必须用copyWith生成新对象替换列表元素,再setState。
6.3 删除分类时的级联处理:不能删就算了
删除分类看似最简单,实际是最需要业务判断的地方。如果某个分类下还挂着条目,直接删掉分类会导致条目失去归属,显示时就会出bug。
我的处理策略是三层防线:
第一层,删除前弹确认框,提示分类下的条目数量。
第二层,如果分类下有未归档条目,不允许删除,而是提示用户先转移或归档这些条目。这个在代码里是一个简单的计数判断:
final items = await ItemRepository().loadByCategory(category.id); if (items.isNotEmpty) { // 提示先转移条目,禁用删除 return; }第三层,为了让“删除”这个动作有后悔药可吃,我用的是软删除。所谓软删除,就是删除时只把isArchived置为true,分类从列表里消失,但数据还在。用户可以在“回收站”里恢复。只有回收站里的分类才会真正从存储里清掉。这在数据安全上多了一道保障,也符合我对生活助手类应用的一贯产品态度——用户数据宁可留冗余,也不要造成不可逆损失。
7. 踩坑记录:编译问题、性能优化与值得注意的细节
7.1 编译期两大拦路虎:SDK路径与Gradle配置
先说编译期的问题。OpenHarmony工程的构建,最老牌的报错就是找不到原生SDK报错。我遇到的错误信息大意是找不到native工具链或SDK路径为空,究其原因就是环境变量没设置或者版本和DevEco Studio不匹配。
还有一个早期版本的Flutter ohos工程会自动生成一个android目录,虽然它不会参与构建,但会干扰编译时的宿主检测。遇到这种情况,可以直接把android目录删掉,只保留ohos目录,编译会清爽很多。
7.2 运行期性能:Future回调与UI刷新时机
运行期的坑,都集中在Dart异步和UI刷新上。有朋友问过我,Future的then回调是不是一定在微任务队列里执行?这个问题的实际影响是:如果你在then回调里直接访问BuildContext,而这个页面已经被销毁,就会报错。
Future的then回调确实是放入微任务队列的,这意味着只要当前事件循环的同步代码跑完,就会立刻执行回调,不等下一个事件循环。但微任务回调执行时,如果页面已经销毁,mounted已经被置为false,你在回调里调用setState就会触发异常。
所以我在所有异步接口返回后的回调里,都会加一层if (!mounted) return;的判断:
Future<void> _loadCategories() async { final list = await CategoryRepository().loadAll(); if (!mounted) return; setState(() { _categories = list; }); }这个习惯建议你从一开始就养成,尤其是在页面频繁跳转的App里,它是运行时崩溃率最高的来源之一。
7.3 列表性能:图片缓存与卡片复用
分类卡片如果用了用户相册图片,第一次滑动时会出现明显的卡顿。原因是每次进页面都是从原生侧重新读取图片字节流,没有任何缓存。
我在分类图片这一块做了两层缓存。第一层是内存缓存,用一个Map<String, Uint8List>存最近的图片字节;第二层是文件缓存,落到应用的缓存目录,下次启动也能命中。核心代码封装在一个ImageCacheManager里,调用方只管传分类id和图片路径,内部自动处理缓存逻辑。
另外在ListView的构建上,务必保证每一项的key稳定。我的卡片统一用ValueKey(category.id),避免Widget复用时状态错乱。
7.4 从OpenHarmony适配经验反推项目架构的建议
做完这个项目,我最大的架构层面的感受是:在Flutter for OpenHarmony还没有到“复制粘贴插件就能用”的成熟度之前,你的代码结构需要比纯Android/iOS开发更严谨。
具体建议有三条:一是把数据访问层和UI层彻底解耦,这样底层存储方案从shared_preferences迁到sqlite时,UI不用动;二是把平台通道的调用全部收口到独立的Service类里,不要在Widget里直接写MethodChannel的调用代码,否则排查问题时要满项目找;三是给每个平台通道做错误兜底,原生侧能力暂时不可用时,App要能优雅降级,而不是直接闪退。
说到底,Flutter的优势在于跨端一致性和开发效率,而OpenHarmony给了这个优势一个新的落点。现阶段它还不是一个完全成熟的生态,但核心能力已经足够支撑真实业务了。分类管理这个功能,从数据模型、存储、界面到原生交互,完整跑下来之后,我对自己下一个OpenHarmony项目的信心明显更足了。如果你也打算在这个方向投入,我建议从分类管理这种数据驱动型功能入手,它足够典型,能帮你把整个技术链路摸一遍,之后再扩展其他功能就是水到渠成的事了。