开源生态里做跨端开发,最头疼的往往不是业务逻辑本身,而是平台适配那一层看不见的坑。Flutter for OpenHarmony这组合,我断断续续折腾了小半年,从工程跑不起来到最终在真机上稳定运行,中间踩过的坑比写业务代码的时间还多。这篇就拿一个生活助手App里最不起眼、但信息量很足的“分类管理功能”来说事,把Flutter在OpenHarmony上的工程搭建、数据持久化、UI交互、组件通信到真机调试的完整链路过一遍。内容适合两类人:一是刚把Flutter环境跑起来、想在OpenHarmony设备上做点实际功能的移动端开发者,二是已经在鸿蒙生态里用ArkTS写过应用、想看看Flutter跨端方案在这套系统上能走多远的同学。代码和思路都从实际项目里来,能直接照着改。
1. 分类管理功能背后的设计思路与技术选型
1.1 一个“简单”功能里藏着的三个技术难点
分类管理在生活助手App里通常长这样:首页一个分类列表,每项有图标、名称、类型,点击进入对应明细,右上角能新增、编辑、排序、删除。表面看是标准CRUD,但落到Flutter for OpenHarmony这个特定组合里,就有三个不省心的点。
第一个是数据持久化。OpenHarmony自带的关系型数据库接口是@ohos.data.relationalStore,ArkTS工程里用起来没问题,但Flutter侧没法直接调这套API,得走通道或者用第三方库封装。如果直接用Flutter生态里的sqflite,默认依赖的是Android的SQLite实现,在OpenHarmony上根本编不过。这就逼着你去翻社区的ohos分支,或者干脆自己用MethodChannel把关系型数据库的能力桥接出来。
第二个是图标和资源的加载路径。OpenHarmony上Flutter应用的资源打包路径、图标字体加载方式与Android有细微差异。分类功能免不了要选图标、配颜色,处理不好就是运行时图标全变方块,或者资源读取报错。这属于不跑真机绝对发现不了的一类问题。
第三个是列表拖拽排序和频繁增删改时,Flutter的列表组件与平台手势之间的冲突。OpenHarmony的触摸事件参数和Android有细微差别,ReorderableListView这类组件需要做小幅度适配,否则长按拖拽的触发阈值和震动反馈都怪怪的。
把这三点拆开看,分类管理就不再是“写个ListView加几个弹窗”的小事了,它正好覆盖了数据层、资源层、交互层三个方向的适配问题,非常适合作为验证Flutter在OpenHarmony上可用性的试金石。
1.2 为什么选择Flutter而不是ArkTS或uni-app
选Flutter for OpenHarmony做这个功能,不是因为它比ArkTS更“高级”,而是因为项目里有一大套已经写好的Flutter业务代码,包括图表组件、登录模块、网络层,全部重写成ArkTS的成本太高。Flutter的ohos适配分支(社区通常叫flutter_flutter的ohos分支或者OpenHarmony sig-maintained版本)存在的意义,就是让这套代码尽量少改移植过去。
另一个原因是团队里很多人已经熟悉Dart和Flutter的组件模型,学习曲线比ArkTS平缓。ArkTS的声明式UI和状态管理本身也很成熟,但如果团队本身没有鸿蒙原生开发经验,强行切过去反而容易在状态管理和生命周期上翻车。Flutter的BuildContext、setState、InheritedWidget这套东西大家用得顺手。
当然也必须承认,Flutter在OpenHarmony上目前还不是官方一等公民,部分系统能力(比如直接调用分布式软总线、超级终端协同)需要通过PlatformChannel桥接,做不到像ArkTS那样天然融合。所以选型结论很明确:如果你是从零开发一个只跑在鸿蒙设备上的应用,ArkTS是更稳的路;如果你有存量Flutter代码,或者未来还想覆盖iOS、Android等多端,Flutter for OpenHarmony就是性价比最高的选择。
1.3 分类管理模块的整体架构设计
我这边最终采用的分层结构是这样的:
- UI层:分类列表页、添加/编辑弹窗、图标选择器、排序设置页,全部由Flutter Widget构建。
- 状态层:使用
Riverpod管理分类列表数据,配合ChangeNotifier监听数据变化。 - 数据层:封装
CategoryRepository,统一提供增删改查接口,内部屏蔽具体存储实现。 - 存储层:默认走
sqflite的OpenHarmony适配分支,同时用shared_preferences缓存少量配置项。
这么做最大的好处是:如果某天OpenHarmony的数据库适配分支出问题,只需要替换存储层实现,Repository接口不变,UI完全感知不到。
final categoryRepository = CategoryRepository(); // UI调用示例 final list = await categoryRepository.getAllCategories();数据流方向也很简单:页面加载时从Repository拉数据,写入Riverpod的State;用户操作触发方法调用,先更新数据库,成功后更新内存State,UI自动刷新。严禁直接改State绕过数据库,否则会出现重启App后数据丢失这种“灵异事件”。
2. 工程搭建与运行环境准备
2.1 工具链清单与版本选择
先在前面说个结论:Flutter for OpenHarmony的环境搭建,网上教程不少,但版本匹配极其敏感,照着老教程配新版工具链大概率跑不起来。我这里用的组合是:
- OpenHarmony SDK:API 10(4.0 Release),对应DevEco Studio 4.0。
- Flutter SDK:OpenHarmony社区维护的ohos分支,版本对应Flutter 3.7左右。
- 开发IDE:DevEco Studio负责OpenHarmony工程编译和真机调试,VS Code负责写Dart代码。
- 真机:Dayu 200开发板(或RK3568系列),系统版本和SDK一致。
有个容易忽略的点:OpenHarmony SDK需要你手动安装ohos-sdk并配置HarmonyOS相关环境变量。很多人在这一步直接用DevEco Studio默认路径,结果命令行里flutter doctor永远检测不到OpenHarmony平台,项目创建后压根没有ohos目录。
命令行工具方面,需要把sdk\default\openharmony\toolchains加到PATH里,同时确认node和hvigw可用。hvigor是OpenHarmony的构建工具,相当于Android里的Gradle,后面很多编译问题都跟它有关。
2.2 创建并配置ohos工程的几个关键步骤
用社区分支的Flutter SDK创建项目时,不能直接执行flutter create完事,还需要手动添加OpenHarmony平台目录。我的做法是:
- 用
flutter create生成标准Flutter工程,语言选Dart,平台只勾android和ios(先不碰ohos)。 - 把OpenHarmony工程模板拷贝到工程根目录,重命名为
ohos,并修改里面的build-profile.json5,把signingConfigs指向你自己的签名文件。 - 在
pubspec.yaml里引入适配过的插件分支,比如sqflite用git方式指向ohos适配仓库。 - 修改
ohos/entry/src/main/module.json5,确认deviceTypes包含phone或tablet,否则装不上真机。
签名这一步容易被跳过,但实际上非常关键。OpenHarmony不像Android可以随便装debug包,真机安装必须有过签名的hap包。没有配置签名文件的话,后面hvigor虽然能出包,但装上设备后会提示安装失败或者验证失败。
注意:OpenHarmony的调试签名需要在DevEco Studio里生成,生成后把
.cer、.p12、.p7b三个文件路径填到build-profile.json5对应的signingConfigs里即可。别用自动生成的临时签名去跑长时间测试,会有过期时间。
2.3 首次运行必须检查的三件事
工程配置完,第一件事不是写代码,而是跑通一个空应用的真机安装流程。我建议你依次检查三个东西,能省掉后面大量排查时间。
第一,检查flutter devices能否识别到OpenHarmony设备。正常情况会列出类似OHOS device的项,如果看不到,多半是hdc(鸿蒙设备连接工具)没有启动,或者设备没开启开发者模式。在DevEco Studio里能连上设备的话,命令行里一般也没问题。
第二,检查flutter run -d ohos能不能热重载。OpenHarmony分支的热重载能力比Android上弱一些,但基本可用。如果一运行就崩,先看是不是签名问题——报Install Failed: error: signature verification failed这类错误时,九成是签名没配对。
第三,检查控制台输出的日志等级。OpenHarmony的hilog和Android的logcat格式不同,Flutter的print输出在hilog里默认可能被过滤掉。我习惯在跑之前加上--verbose参数,或者直接用DevEco Studio的Log窗口过滤Dart关键字,否则很多运行时错误根本看不到。
这三件事都通过后,才算真正具备开发条件。很多时候所谓的“Flutter新建项目后跑不起来”,其实都死在这三个环境细节上,跟业务代码一点关系都没有。
3. 分类管理的数据层设计
3.1 分类数据模型该怎么定义
分类业务上一般有两种:支出分类和收入分类。生活助手App里购物、餐饮、交通是支出,工资、理财收益是收入。我在模型里加了一个type字段区分,后续统计报表会按这个字段分组。
Dart模型定义如下:
class Category { final int? id; final String name; final String icon; final int iconColor; final int type; // 0=支出, 1=收入 final int sortOrder; final bool isDefault; Category({ this.id, required this.name, required this.icon, required this.iconColor, required this.type, required this.sortOrder, this.isDefault = false, }); Map<String, dynamic> toMap() { return { 'id': id, 'name': name, 'icon': icon, 'icon_color': iconColor, 'type': type, 'sort_order': sortOrder, 'is_default': isDefault ? 1 : 0, }; } factory Category.fromMap(Map<String, dynamic> map) { return Category( id: map['id'], name: map['name'], icon: map['icon'], iconColor: map['icon_color'], type: map['type'], sortOrder: map['sort_order'], isDefault: map['is_default'] == 1, ); } }icon字段我存的是图标字体的Unicode码点字符串,比如'0xe600',而不是直接存图片路径。这样换主题、换图标库时不用改数据库,UI层按码点渲染即可。iconColor存的是ARGB整数值,Flutter的Color可以直接用,但要注意OpenHarmony上字体渲染对透明通道的处理跟Android不完全一样,部分设备上Color(0x00000000)会渲染成纯白,建议颜色值全部带不透明alpha。
3.2 存储方案选型:为什么盯上sqflite适配分支
数据存储这边,我首推的还是SQLite。OpenHarmony自身有关系型数据库,但Flutter侧要跨语言调用,信息密度和事务控制都会受限。SQLite在Flutter生态里足够成熟,SQL语法统一,后期如果要同步到服务端,迁移成本也低。
但直接写import 'package:sqflite/sqflite.dart'在OpenHarmony上是编译不过的,因为原版sqflite底层依赖Android的SQLite实现。OpenHarmony社区里有人维护了适配版,通过ffi直接调OpenHarmony的native SQLite接口。用的时候需要在pubspec.yaml里写成:
dependencies: sqflite: git: url: https://gitee.com/xxx/sqflite_ohos.git ref: ohos_main加载的插件名也会变化,初始化时记得用openDatabase的factory参数指定:
final db = await openDatabase( path, version: 1, onCreate: (db, version) async { await db.execute(''' CREATE TABLE categories( id INTEGER PRIMARY KEY AUTOINCREMENT, name TEXT NOT NULL, icon TEXT, icon_color INTEGER, type INTEGER, sort_order INTEGER, is_default INTEGER ) '''); }, );其实还有一个更轻量的思路:分类数据量通常很小,几十条撑死,用shared_preferences存JSON也不是不行。但如果App未来还要加流水记录、账单分析,分类表迟早要跟其他表做关联查询,所以一步到位上SQLite是更稳的选择。
提示:如果实在不愿引第三方适配库,也可以自己用
MethodChannel桥接OpenHarmony的relationalStore。工作量大概多一两天,但要处理异步回调、数据类型转换、事务边界,我这边实测下来还是直接用适配分支省心。
3.3 默认分类初始化与Repository封装
首次启动时,分类表是空的,需要在用户看到页面之前插入一组默认分类。这个逻辑不建议直接写在页面初始化里,而是收敛到CategoryRepository的initDefaultData()方法。
class CategoryRepository { static const _defaultCategories = [ {'name': '餐饮', 'icon': '0xe601', 'color': 0xFF4CAF50, 'type': 0}, {'name': '购物', 'icon': '0xe602', 'color': 0xFFFF9800, 'type': 0}, {'name': '交通', 'icon': '0xe603', 'color': 0xFF2196F3, 'type': 0}, {'name': '工资', 'icon': '0xe604', 'color': 0xFF009688, 'type': 1}, ]; Future<void> initDefaultData(Database db) async { final count = Sqflite.firstIntValue( await db.rawQuery('SELECT COUNT(*) FROM categories'), ); if (count == 0) { final batch = db.batch(); for (final c in _defaultCategories) { batch.insert('categories', { 'name': c['name'], 'icon': c['icon'], 'icon_color': c['color'], 'type': c['type'], 'sort_order': 0, 'is_default': 1, }); } await batch.commit(noResult: true); } } }为什么用count == 0作为判断条件而不是单独存一个“是否初始化”的标记?因为如果用户删光了所有默认分类,再重启App,标记法会重新插一遍默认数据,用户会以为App出bug了;用count判断则尊重用户的操作结果。这个小细节我是在用户反馈“删光分类后重启又冒出来”之后才改掉的。
Repository对外只暴露getAllCategories、insertCategory、updateCategory、deleteCategory、reorderCategories这五个方法,内部全部用Future异步。所有数据库路径都通过getDatabasesPath()拼接,不要写死绝对路径,OpenHarmony的沙箱路径跟Android不一样,写死了一定挂。
4. UI交互实现:列表、弹窗与图标选择
4.1 分类列表主页面布局与手势冲突处理
主页面结构不复杂:顶部标题栏,下面一个ReorderableListView,每个列表项显示图标、名称、类型标签,右侧一个拖拽手柄。但OpenHarmony上这个列表的拖拽体验和Android有差异,主要表现为长按触发拖拽时,列表滚动与拖拽的判定经常打架,很容易把“滚动”误判成“拖拽”。
解决方案是使用ReorderableListView.builder,并把buildDefaultDragHandles设为false,自己包一个拖拽图标作为ReorderableDelayedDragStartListener的子组件:
ReorderableListView.builder( buildDefaultDragHandles: false, itemCount: categories.length, onReorder: (oldIndex, newIndex) { setState(() { if (newIndex > oldIndex) newIndex -= 1; final item = categories.removeAt(oldIndex); categories.insert(newIndex, item); }); _repository.reorderCategories(categories); }, itemBuilder: (context, index) { final item = categories[index]; return ReorderableDelayedDragStartListener( key: ValueKey(item.id), index: index, enabled: !item.isDefault, // 默认分类不可拖拽 child: _buildCategoryItem(item), ); }, )注意onReorder里那个newIndex > oldIndex时要减一的逻辑。这是ReorderableListView老生常谈的坑:向下拖动时,newIndex传入的是移除前的位置,必须修正,否则顺序永远错一位。我在OpenHarmony上实测,这个坑依然存在,没被适配分支修掉。
默认分类不可拖拽这一点,我通过enabled: !item.isDefault来控制。用户反馈里经常有人误操作把“餐饮”拖到最底部,然后统计报表排序乱了又来投诉,干脆直接禁掉最稳。
4.2 添加与编辑分类弹窗的实现细节
添加和编辑可以用同一个弹窗组件,通过传入的Category?参数区分。弹窗里核心就三块:名称输入框、类型切换、图标选择入口。
名称输入框有一个容易踩的坑:OpenHarmony的输入法弹起时,弹窗组件可能会被顶出屏幕。Flutter的showDialog在Android上默认会resizeToAvoidBottomInset,但在OpenHarmony适配分支上,这个特性并不可靠。我的解决办法是给弹窗内容包一层SingleChildScrollView,并监听MediaQuery.of(context).viewInsets.bottom动态调整底部padding:
Padding( padding: EdgeInsets.only( bottom: MediaQuery.of(context).viewInsets.bottom, ), child: SingleChildScrollView( child: _buildForm(), ), )类型切换用SegmentedButton就行,Android上这个组件表现良好,OpenHarmony上只要色彩主题不是太偏门,也没问题。不过要注意,SegmentedButton是Material 3组件,如果项目里用的是Material 2主题,要先升级主题,否则运行时会直接抛异常。
保存逻辑统一走_repository.insertCategory或updateCategory,成功后Navigator.pop,并在弹窗打开的页面里通过await showDialog(...)拿到返回值后刷新列表。不建议在Repository里直接发通知更新状态,因为弹窗可能同时存在多个实例,返回值的方式更直观可控。
4.3 图标选择器与颜色配置的交互方案
分类图标这里我踩过大坑。一开始图省事,用Flutter内置的Icons字体,结果OpenHarmony上部分图标字形渲染异常,显示成方块。后来改成自托管图标字体:从阿里iconfont里挑一批分类图标,生成ttf放进assets/fonts,在pubspec.yaml里声明后,用IconData自定义码点渲染。
字体声明:
flutter: fonts: - family: CategoryIcons fonts: - asset: assets/fonts/category_icons.ttf运行时,Icon(IconData(codePoint, fontFamily: 'CategoryIcons')),其中codePoint就是从数据库读出来的0xe601之类的值。这样图标、颜色都数据化了,用户自定义分类时甚至可以自己选颜色。
图标选择器我用的是一个横向滚动的GridView弹层,按类型分组展示。交互上有个细节:选完图标后立刻反馈到列表预览,不要等用户点“确定”才生效。因为分类功能的变更频率高,反馈快一点,用户会觉得应用响应迅速。
颜色选择做成了预设色板,8个纯色加上透明度选项。注意颜色存数据库时用int即可,但OpenHarmony某些设备在渲染低饱和色时有色偏,我实测下来0xFF开头的纯色最稳,太花哨的渐变效果在低端开发板上会掉帧。
5. 状态管理与组件通信:从setState到Riverpod
5.1 为什么分类列表不能只用setState硬扛
早期的分类页面只有几十条数据,我用setState维护列表,修改列表项直接改内存数组,看起来没什么问题。但后来加了统计页面之后,分类列表的变更需要同时刷新首页的收支汇总、账单页的下拉筛选,再靠setState一层层往上回调,代码就变成了一坨。
Flutter官方的做法是状态提升,但提升到根Widget之后,所有子组件都跟着重建,性能损耗虽然分类场景下感觉不到,但代码可维护性很差。是时候引入统一的状态管理方案了。
我在这个工程里用的是Riverpod,理由是它对异步操作的支持比Provider更自然,FutureProvider可以优雅地处理“从数据库读数据”这种场景,不需要手动管理loading状态。当然,如果你更熟悉Provider或者Bloc,也完全可以,核心思路是一致的:把分类列表抽成全局单例状态,任何组件都能监听,任何组件都能通过Provider内的方法修改。
5.2 Flutter组件通信的几种姿势对比
在分类管理这个模块里,我实际用到的组件通信方式有三种,按场景区分:
第一种是父传子,直接构造参数传递。比如列表项CategoryItem需要接收Category对象和拖拽回调,直接在构造函数里传,简单直白。这种方式的缺点是深层次传递时需要一层层透传,但项目里只有两层,问题不大。
第二种是子传父,用回调函数。弹窗保存成功之后,通过Navigator.pop(result)把结果抛回给列表页,列表页根据返回值执行刷新操作。这个模式适合一对一、一次性的通信,代码流程清晰,不容易出bug。
第三种是跨组件状态同步,用Riverpod的StateNotifierProvider。分类页和统计页都要监听同一份分类数据,这时候用回调就会乱套——统计页怎么拿到分类页的更新通知?用全局Provider就干净了:
final categoryListProvider = StateNotifierProvider<CategoryListNotifier, List<Category>>((ref) { return CategoryListNotifier(); }); class CategoryListNotifier extends StateNotifier<List<Category>> { CategoryListNotifier() : super([]); Future<void> load() async { final repo = CategoryRepository(); state = await repo.getAllCategories(); } Future<void> add(Category category) async { final repo = CategoryRepository(); final id = await repo.insertCategory(category); state = [...state, category.copyWith(id: id)]; } Future<void> remove(int id) async { await repo.deleteCategory(id); state = state.where((c) => c.id != id).toList(); } }关键点:所有数据库操作成功后,才更新state。先改state再写库,一旦数据库写入失败,界面和数据就分叉了。我见过不少新手在这里翻车。
5.3 数据更新后UI自动同步的坑
Riverpod用起来之后,大部分UI同步问题都消失了,但有个隐藏坑值得单独说:列表项的key问题。
ReorderableListView要求每个item必须有唯一的key,我用的是ValueKey(item.id)。但如果新增分类后,数据库返回的id是自增的,可能和之前删除的某个分类id重复(虽然SQLite自增不会立即复用,但某些情况下会)。一旦key重复,列表会出现诡异的复用错乱——图标对不上名字,拖拽后条目乱跳。
解决方法是,插入后立刻用数据库返回的id构建新对象,并且给列表项加一个不可变的uniqueKey字段,用uuid生成,彻底避免依赖自增id。这个小改动花了我半小时,但排除了一个极难复现的线上bug。
组件通信这件事,原则就是:能用参数传递就别引入全局状态,能用局部回调就别把事件总线引进来。项目规模没到一定复杂度时,过度设计比不用设计更坑。
6. 真机调试与常见问题排查实录
6.1 “跑不起来”的三大元凶
Flutter for OpenHarmony最常见的问题,永远是“新建项目后跑不起来”。我帮别人排查过好几次,九成都是下面三个原因之一。
第一是SDK路径没配对。DevEco Studio装的鸿蒙SDK路径和命令行里flutter doctor检测到的路径不一致,或者PATH里没有sdk/default/openharmony/toolchains,导致hdc找不到。检查方法很简单:命令行执行hdc list targets,有设备输出就说明连接工具没问题。
第二是签名配置缺失或过期。OpenHarmony真机安装应用必须有签名,调试签名有效期通常只有三个月。如果hvigor构建时没报错,但安装到设备提示Install Failed: info: error: verify signature failed,那就是签名过期了,去DevEco Studio重新生成即可。
第三是工程模板版本不匹配。OpenHarmony API版本和Flutter ohos分支版本是绑定的,API 9的工程配API 11的SDK,或者反过来,都会在编译阶段报错。选型时先锁定一组:我在2.1节给出的那组组合实测最稳,你自己搭环境时,优先找对应版本的配套文档,别混搭。
6.2 Dart运行时与资源加载报错的处理
运行阶段报错里,出现频率最高的是类似这种:
E/flutter (31173): [ERROR:flutter/runtime/dart_vm_initializer.cc(41)] Unhandled exception后面通常会跟着一个Dart堆栈。大部分时候这是业务代码抛的异常被Flutter全局捕获了,但OpenHarmony上有一个特殊原因:dart_vm_initializer.cc报错往往伴随着isolate初始化失败,尤其是用了多个isolate做后台计算时,OpenHarmony适配分支的isolate支持还不太完善。
我的建议是:分类管理这种轻量操作尽量别开新isolate,直接用async/await就够了。如果必须走isolate,测试时重点验证真机场景,模拟器上不稳定的问题真机上可能更严重。
另一个资源加载类报错是字体清单问题:自定义图标字体在pubspec.yaml里声明了,但运行时图标不显示。排查顺序是:先看assets/fonts路径对不对,再看family字段是否和代码里fontFamily完全一致,最后确认字体文件本身格式是否为ttf且包含对应码点。有时候Android上能用的otf字体,在OpenHarmony上渲染成方块,换ttf格式立刻就好了。
6.3 构建任务依赖解析失败的排查思路
用hvigor构建时,偶尔会遇到类似“could not determine the dependencies of task”的错误,OpenHarmony上通常是ohos模块的依赖解析问题。这次跟Android的Gradle依赖冲突不一样,hvigor的依赖解析对网络和仓库地址更敏感。
第一反应是检查ohos/oh-package.json5里的依赖版本是否与SDK版本匹配,特别是@ohos/hypium(测试框架)和@ohos/hvigor-ohos-plugin。这两个组件版本跟API版本强绑定,写错一个字母就会导致整个依赖树解析失败。
第二是检查网络。OpenHarmony的依赖仓库在gitee上,部分地区访问不稳定。如果构建日志里出现connect timeout或者unresolved dependency,多试几次或者配置镜像源。这个属于环境问题,不是代码问题,先别急着改代码。
第三是检查模块目录结构。entry/src/main目录下的module.json5、abilities配置影响任务依赖,如果某次合并代码时把module.json5改成非法JSON,hvigor会报一个看不懂的“task dependency”错误,实际就是配置文件解析失败。用DevEco Studio打开配置文件看有没有红波浪线,比看堆栈直观得多。
6.4 PlatformView与摄像头等系统能力的适配问题
分类管理本身不需要摄像头,但生活助手App后续要接入扫码记账、拍照识别票据,这里提前说一下PlatformView的坑。
Flutter在OpenHarmony上的PlatformView,机制跟Android上类似,也支持混合合成(Hybrid Composition),但性能表现有明显差距。我测试过在列表页里嵌入一个相机预览,OpenHarmony上滑动列表时预览画面会卡顿,而且相机画面的旋转方向跟Android相反,前置摄像头取景经常是倒的。
如果业务上也遇到这个问题,排查思路是:先用Texture方式渲染,如果不行再切AndroidView的混合合成模式;同时检查设备传感器方向配置是否在module.json5里声明了orientation。这个问题没有通用解决办法,每家设备的表现都可能不一样,只能真机逐个调。
6.5 上架前的兼容性验证:XTS与多设备适配
最后提一下OpenHarmony的XTS认证。如果应用要上架到官方应用市场,或者在企业内部大规模分发,通常需要过XTS兼容性测试。这个测试会验证应用在不同设备上的兼容性表现,包括API调用、权限使用、资源加载等。
我在分类管理功能上遇到过一个XTS相关的问题:测试用例里会遍历应用的每个页面,检查是否存在内存泄漏或未释放的资源。由于分类弹窗用了showDialog且没在关闭时正确销毁Controller,导致某台测试设备上弹窗多次开关后内存明显上涨。后来我在dispose里把所有TextEditingController和FocusNode都释放掉,才过了测试。
多设备适配方面,OpenHarmony的设备类型差异很大,从手机到平板到开发板,屏幕尺寸和像素密度都不一样。分类列表在手机上一个ListView就够,但在平板上会显得很空。我的处理方式是按屏幕宽度判断,超过600dp时切成两列GridView,复用同一个数据源和Repository。这个逻辑用在真机上验证过,体验比单一列表好不少。
写在最后的经验打包
这些是在OpenHarmony真机上反复折腾后留下的几条硬经验。第一,版本一切以“能跑通空工程”为基准,不要追新。我见过太多人非要装最新的Flutter ohos分支,然后被一堆编译错误淹没。第二,数据层和UI层一定要解耦,分类这种看似简单的功能,将来加字段、加统计、加云同步,都是很自然的需求,Repository模式能让你不用推翻重写。第三,真机比模拟器重要一百倍,OpenHarmony的模拟器在UI渲染和输入法行为上与真机差异巨大,凡是涉及拖拽、弹窗、键盘的调整,都值得提前插上真机测一测。
最后分享一个小技巧:调试分类列表刷新问题时,可以在Riverpod的StateNotifier里临时加一句print(state.map((c) => c.name).toList()),每次增删改后看输出顺序是否符合预期。这比在UI层打断点快得多,因为很多刷新异常根本到不了UI就已经错了。整个项目做下来,我对Flutter在OpenHarmony上的成熟度比一开始乐观了不少——它确实还带着不少工程期的小毛病,但应付生活助手这类常规应用场景已经完全够用,而且一套Dart代码跑遍Android、iOS、OpenHarmony这件事,对中小团队来说太有吸引力了。