news 2026/10/2 21:59:01

Flutter适配OpenHarmony实战:生活助手分类管理完整实现

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
Flutter适配OpenHarmony实战:生活助手分类管理完整实现

从去年开始,我陆续接到好几个朋友的需求,都是同一个意思:自己用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.js14.19.1及以上,编译工具链需要
hdc工具随DevEco Studio安装,用于真机部署
DevEco Studio4.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 生活助手里“分类”到底管哪些事

分类管理看着简单,但凡是做过实际项目的人都知道,这个模块牵扯的东西一点都不少。在我这个生活助手里,分类被用在三个场景:

  • 记账:支出类别(餐饮、交通、购物、医疗等)和收入类别(工资、理财、兼职等)。
  • 待办事项:工作、学习、家庭、健康等任务分组。
  • 物品管理:比如家里的杂物、工具、药品,按存放位置或类型归类。

所以我在设计分类模型时,刻意没有把分类和某个具体业务场景绑定死,而是定义成一个通用的实体。每个分类包含下面这些字段:

字段类型说明
idString主键,用UUID
nameString分类名称
iconString图标,存路径或Emoji字符
colorValueint分类卡片背景色
sortOrderint排序权重,越小越靠前
isDefaultbool是不是系统预置分类
isArchivedbool是否已归档,软删除标识
createdAtint创建时间戳

这套设计的好处是:后续不管是做统计报表还是做多场景复用,都不需要大改数据结构。

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项目的信心明显更足了。如果你也打算在这个方向投入,我建议从分类管理这种数据驱动型功能入手,它足够典型,能帮你把整个技术链路摸一遍,之后再扩展其他功能就是水到渠成的事了。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/10/2 21:58:07

频率域图像处理核心:傅里叶变换、频域滤波与同态滤波全解析

数字图像处理这门课&#xff0c;理论上讲&#xff0c;前几章再零碎&#xff0c;大家照着例题还是能把作业写出来的。但到了第四章频率域图像处理&#xff0c;大多数人的反应会突然慢下来&#xff1a;坐标系成了 u、v&#xff0c;图像变成了复数矩阵&#xff0c;之前积累的线性代…

作者头像 李华
网站建设 2026/10/2 21:58:06

YOLOv8行人车辆检测数据集实战:从验证集划分到模型训练

简介&#xff1a;面向行人与车辆检测任务的中型标注图像数据集&#xff0c;内含4500张训练图和500张验证图&#xff0c;适合使用YOLOv5等深度学习模型进行目标检测训练与效果评估。压缩包共16821个文件&#xff0c;其中jpg原图5607张&#xff0c;txt格式YOLO标注5607个&#xf…

作者头像 李华
网站建设 2026/10/2 21:56:49

MySQL SSL加密访问配置实战:从证书生成到强制加密完整指南

给MySQL配置SSL加密访问&#xff0c;这事儿我拖了大半年才动手。理由其实很实在&#xff1a;数据库在内网&#xff0c;觉得没人会无聊到去监听交换机流量。直到有一次闲着没事&#xff0c;在自己搭的测试环境里用Wireshark看了一轮客户端和服务端的交互&#xff0c;结果一条UPD…

作者头像 李华
网站建设 2026/10/2 21:53:32

Unity点击事件与UI穿透冲突的通用修正方案

站在Unity开发者的角度&#xff0c;点击事件和UI“打架”这个问题&#xff0c;尤其是“UI弹出时穿透点击到场景物体”“按钮连点触发多次”这两个症状&#xff0c;几乎每个项目都会遇到。我最早做2D手游的时候就吃过亏&#xff1a;玩家疯狂点“关闭”按钮&#xff0c;结果把按钮…

作者头像 李华
网站建设 2026/10/2 21:50:01

Linux磁盘占用排查:du命令从基础到实战完全指南

今天聊一下 Linux 下最常用的磁盘占用排查命令&#xff1a;du。不管你是在生产服务器上看到报警“磁盘空间不足”&#xff0c;还是本地开发环境莫名撑爆了根分区&#xff0c;总得先搞清楚哪个文件、哪个目录把空间给吃掉了。du 就是干这个的。 这个命令的核心能力是递归统计文…

作者头像 李华
网站建设 2026/10/2 21:49:50

SpringBoot+Vue在线教育系统源码:从环境搭建到二次开发全攻略

把这套项目真正跑起来之前&#xff0c;我先说一句大实话&#xff1a;网上号称"可直接运行"的源码很多&#xff0c;但绝大多数你都要花一晚上解决数据库版本、端口冲突、前端代理这三个问题。这套在线教育系统信息管理系统源码&#xff0c;SpringBoot 后端 Vue 前端 …

作者头像 李华