ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

OpenHarmony上Flutter收藏功能实战:状态管理与持久化全解析

OpenHarmony上Flutter收藏功能实战:状态管理与持久化全解析 1. 需求拆解收藏功能为什么值得单独讲做垃圾分类指南App的初期我把它想得挺简单按类别查垃圾、搜一下名称、给个投放说明这就是全部。可真到把项目从Android切到OpenHarmony平台并且完整实现“我的收藏”时我才意识到这个不起眼的小功能几乎是整个App里最长心智负担的部分。原因在于收藏状态要同时出现在首页卡片、搜索列表、详情页和独立的收藏管理页里而用户在一个页面的操作必须实时反映到其他所有页面。这意味着它天生需要一套可靠的全局状态管理方案再加上跨端数据持久化、平台差异适配复杂度一下就上来了。这篇文章不是教你搭一个完整App而是把“我的收藏”这条业务线从头到尾的实战过程拆开从需求设计、工程搭建、状态管理、页面实现到OpenHarmony上的适配坑和手段。如果你正在做Flutter for OpenHarmony项目或者打算把现有Flutter App移植到鸿蒙生态这部分经验可以直接拿去做参考。下面我尽量按真实开发的时间顺序来讲哪些地方我踩过坑、哪些代码后来精简掉了都会交代清楚。1.1 垃圾分类指南App的业务模型先交代一下项目背景。这个App的核心数据是“垃圾条目”也就是一条一条的垃圾名称比如“过期口罩”“矿泉水瓶”“剩饭剩菜”。每条数据对应几个关键字段id、名称、所属分类可回收、有害、厨余、其他、分类标志色、投放说明、常见误区等。用户的使用路径很简单搜索或浏览垃圾条目查看投放说明然后通过“收藏”按钮把常用条目保存到自己的收藏夹里。从业务角度看收藏模块承担的东西并不多但它的数据模型和主业务模型有微妙的关系。一开始我打算直接在垃圾条目表上加一个is_favorite字段后来发现不行。因为垃圾条目本身来自远端数据或者本地资源收藏列表需要保存的是用户的主观行为如果把这两个东西耦合在一起后续一旦要加“收藏时间”“标签备注”等属性就得改动主表结构。所以设计时需要把收藏单独建模一个垃圾条目可以被收藏但收藏状态不应该污染垃圾条目本身的定义。我的最终模型很简单用GarbageItem表示垃圾条目用FavoriteModel管理收藏集合。GarbageItem负责展示数据FavoriteModel负责收藏增删、查重和持久化。这样两个模块各自演进将来如果改成数据库存储或者增加云同步只需要动FavoriteModel这一层。1.2 收藏功能隐含的复杂度从表面看收藏就是一个布尔值点了就加再点就删。可真的落代码你会发现它涉及到的技术点很密集。首先是跨页面共享状态。用户很可能是在首页卡片上点击收藏然后进详情页再取消收藏最后再去收藏页查看结果。这三个页面如果各自维护一份状态一定会出现不同步。而且垃圾分类场景里用户常常会“批量操作”连续收藏好几条在收藏页左滑删除一条还要能一步到位地反映到全局。其次是持久化。用户在收藏页添加的条目过两天打开App必须还在。如果只存在内存里App一杀掉就全没了这在用户眼中就是程序bug。即便用数据库也要考虑OpenHarmony上数据库插件和Android之间的行为差异不是所有插件都能无脑换平台。然后还有防重复、实时反馈、滚动性能等交互细节。收藏按钮点击后如果列表整个重建会在视觉上闪烁用户体验很差。很多看起来“只需一行代码”的页面背后的状态设计和性能考量才是真正花时间的部分。1.3 需求清单从“点星标”到“可管理”为了方便开发时验收我把收藏模块的交互需求逐条列了一下。当时纸上写得很随意但事后看这些条目基本覆盖了所有关键场景首页的垃圾卡片展示星标按钮点击后立即收藏/取消图标状态要实时变化。详情页展示收藏状态切换收藏后返回首页时同一条目状态必须一致。独立“我的收藏”页面能从本地存储加载收藏数据支持左滑删除和清空全部收藏。首次进入或加载过程中有加载态无收藏时有空态提示。App冷启动后收藏数据仍然存在不丢失。快速连续点击收藏按钮不能产生重复收藏或崩溃。这六条看似简单但每一条都对应着一个具体的技术决策。比如第2条需要Provider级的状态共享第5条需要合理的持久化方案第6条则要求在写集合做防重复校验。后面的章节我会按这些需求来展开实现。2. 开发环境搭建与工程初始化2.1 准备OpenHarmony的Flutter开发链如果你之前一直用标准版Flutter开发Android/iOS第一次切到OpenHarmony会很不习惯。最大的差异在于OpenHarmony并不是Flutter官方的第一优先级平台所以要跑起来必须用一个专门的Flutter发行版或分支。我目前用的是OpenHarmony SIG维护的flutter_flutter的ohos分支它和官方Flutter同源但额外支持了ohos平台还能继续构建Android目标这样我可以在同一个工程里维护两个平台。环境搭建的关键步骤是这样的先准备好DevEco Studio和OpenHarmony SDK这是编译和联调的基础然后把ohos分支的Flutter下载到本地配置好PATH最后执行flutter doctor -v看能不能识别到OpenHarmony SDK路径。如果之前装过Android开发环境最好检查一下环境变量里有没有冲突因为安卓SDK和OpenHarmony SDK都叫“SDK”Flutter工具偶尔会认错。git clone -b ohos https://gitee.com/openharmony-sig/flutter_flutter.git export PATH$PATH:$PWD/flutter_flutter/bin flutter doctor -v刚开始我在这里就卡了快半天。flutter doctor一直报找不到OpenHarmony SDK后来发现是DevEco Studio里安装的SDK路径没有写到环境变量里。你需要手动在.bashrc或者.zshrc里设置OHOS_SDK_HOME指向SDK目录然后在Flutter根目录的flutter/bin/cache里清理一下缓存再重新flutter doctor。这还是老传统很多Flutter的环境问题都跟缓存有关系。2.2 创建支持ohos平台的Flutter工程环境搭建到位后创建工程需要用--platforms参数指定ohos。当时的命令大致是flutter create --platforms ohos .如果用的是官方最新支持OpenHarmony的Flutter版本也可以写成flutter create --platforms ohos,android一次生成两个平台目录。生成的目录里会多出一个ohos文件夹里面是OpenHarmony工程类似Android的android目录包含entry、module.json5、build-profile.json5这些文件。你可能还需要手动配置项目的包名OpenHarmony在这里跟Android略有区别它要求的是bundleName格式要和module.json5里保持一致。2.3 环境问题新建项目跑不起来怎么办新手在这阶段最容易遇到两个问题一是flutter run执行后找不到设备二是启动后控制台直接给出E/flutter开头的异常堆栈。找不到设备通常是OpenHarmony的hdc服务没启动或者DevEco Studio没有建立设备连接。先打开DevEco Studio把设备识别出来再去命令行执行flutter devices确认一下。如果列表里有OpenHarmony设备再跑flutter run -d deviceId。第二个问题就很典型了控制台里会出现[ERROR:flutter/runtime/dart_vm_initializer.cc(41)] Unhandled exception这样的日志。看到dart_vm_initializer的时候先别急着去改业务代码这通常是Dart虚拟机初始化阶段或在事件循环里出现了未捕获异常很可能是某个插件在OpenHarmony上还没有原生实现导致调用时直接抛错。我把这部分排障过程留到第5章细说这里先记住一点出现这类日志第一时间看完整堆栈而不是只看第一行。我当时去翻完整日志才发现是path_provider的channel调用没被处理因为默认加载的不是OpenHarmony版本。后来换成适配版就好了。所以“新建项目跑不起来”不一定是Flutter本身的问题更多时候是插件与平台的兼容性问题。3. 数据层与状态管理Provider正确姿势3.1 定义垃圾条目和收藏数据模型在动手写页面之前最好先把数据模型固定下来。我用了最常规的方式一个GarbageItem类带fromJson/toJson方便后面做序列化存储。字段不多就id、name、category、categoryColor、disposalTip。categoryColor用int存色值避免把Color对象直接塞进JSON。class GarbageItem { final String id; final String name; final String category; final int categoryColor; final String disposalTip; const GarbageItem({ required this.id, required this.name, required this.category, required this.categoryColor, required this.disposalTip, }); factory GarbageItem.fromJson(MapString, dynamic json) { return GarbageItem( id: json[id] as String, name: json[name] as String, category: json[category] as String, categoryColor: json[categoryColor] as int, disposalTip: json[disposalTip] as String, ); } MapString, dynamic toJson() { return { id: id, name: name, category: category, categoryColor: categoryColor, disposalTip: disposalTip, }; } }收藏模型我单独建了一个FavoriteModel没有把收藏字段塞进垃圾条目。它内部保存一个ListGarbageItem并提供isFavorite、addFavorite、removeFavorite等方法。这里有个细节值得提一下判断是否收藏时我最初用_favorites.any((item) item.id id)这个写法在数据量几十条时完全没问题最多O(n)线性扫描但如果你未来收藏条目可能上千建议额外维护一个SetString来存id把去重和判断的时间都降到O(1)。这个App的收藏量级不大所以我就没有过度设计。3.2 存储选型为什么先用shared_preferences而不是数据库数据模型的下一步是持久化。我见过很多朋友一上来就上sqlite用数据库存收藏其实没必要。收藏列表的特点是单用户、数量少、读取频繁但并发低、无复杂查询。这种场景用shared_preferences最合适。它内部就是一个键值存储跟Android的SharedPreferences一样写入后App重启还在拿来存JSON数组刚刚好。可能你会问为什么不用HiveHive虽然快但当时在OpenHarmony上的适配还不太成熟初始化阶段偶尔会出奇怪的问题。而shared_preferences有一个专门的OpenHarmony适配版本shared_preferences_ohosAPI跟官方完全一致切换成本几乎为零。权衡下来先选它最稳妥。实际存储结构是这样的用一个固定key比如favorite_list存一个JSON数组。每次收藏列表有变动就把它整体序列化写进去。因为收藏操作是低频行为不需要担心频繁写入带来的性能损耗。如果将来收藏数量变大、需要模糊搜索或按分类过滤再换成sqflite或者drift都不迟收藏页面内部的数据操作都被封装在FavoriteModel里替换成本可控。3.3 用ChangeNotifier封装收藏状态状态管理我选了Provider没有刻意去上bloc或Riverpod。原因很简单收藏模块是全局共享的可写状态用Provider的ChangeNotifier模式最直观学习成本低团队接手无障碍。先写一个FavoriteModel继承ChangeNotifier在add和remove方法里调用notifyListeners()这样所有监听它的组件都能自动刷新。class FavoriteModel extends ChangeNotifier { final ListGarbageItem _favorites []; ListGarbageItem get favorites List.unmodifiable(_favorites); bool isFavorite(String id) { return _favorites.any((item) item.id id); } void addFavorite(GarbageItem item) { if (isFavorite(item.id)) return; _favorites.add(item); notifyListeners(); _save(); } void removeFavorite(String id) { _favorites.removeWhere((item) item.id id); notifyListeners(); _save(); } }在App入口用ChangeNotifierProvider包住整个MaterialApp。这里有一个关键点Provider必须在MaterialApp的上一层也就是根Widget上创建千万不能包在某个具体页面的内部。否则页面跳转时子页面可能找不到Provider实例。我在实际开发中遇到过这个问题后面会在排查章节里详细说。void main() { runApp( ChangeNotifierProvider( create: (_) FavoriteModel()..loadFromStorage(), child: const GarbageGuideApp(), ), ); }3.4 状态变更后的持久化策略持久化不能只停留在“有存储”这一步还要考虑时机和异常。我在FavoriteModel里加了_save()方法Futurevoid _save() async { final prefs await SharedPreferences.getInstance(); final jsonList _favorites.map((item) item.toJson()).toList(); await prefs.setString(favorite_list, jsonEncode(jsonList)); }这个方案很简单但是有两个经验点。第一不要在notifyListeners()之前调用_save()否则如果存储抛异常页面状态已经刷新了容易出现显示和实际不一致。先更新集合再通知UI最后再异步写盘顺序反过来会好一些。第二SharedPreferences.getInstance()本身是异步操作如果App冷启动时立刻去读可能读到的还是空值。所以我在loadFromStorage()里做了一个加载状态启动时先加载加载完再notifyListeners()让收藏页有一个Loading态避免一闪而过的空态误导用户。还有一个值得注意的异常处理。存储的JSON一旦因为版本升级或人为修改变成不合法的格式直接jsonDecode会抛异常。所以loadFromStorage()内部务必要用try-catch包住捕获异常后清空数据并写一个默认空列表而不是让App直接白屏。4. 页面实现与组件通信4.1 列表卡片收藏按钮的动静分离页面实现中最容易踩坑的地方是列表收藏按钮的性能。如果首页的ListView在收藏状态变化时整个重建用户体验会特别糟糕。我的做法是让每个收藏按钮自己监听FavoriteModel而不是让整个ListTile都去context.watch。用ConsumerFavoriteModel包住IconButton这样FavoriteModel发生变化时只有按钮所在的局部区域重建列表的其他项不会被影响。代码如下ConsumerFavoriteModel( builder: (context, favModel, child) { final bool fav favModel.isFavorite(item.id); return IconButton( icon: Icon( fav ? Icons.star : Icons.star_border, color: fav ? Colors.amber : Colors.grey, ), onPressed: () { if (fav) { favModel.removeFavorite(item.id); } else { favModel.addFavorite(item); } }, ); }, )这个模式的另一个好处是不需要在每个卡片构造函数里传收藏状态。卡片只需要一个GarbageItem对象收藏状态从Provider里拿这样就不会因为列表项的复用导致状态错乱。尤其在ListView.builder中item是会被回收复用的如果收藏状态是外部传入的旧数据很容易出现滑动后星标显示错误的bug。4.2 详情页收藏状态与页面通信详情页和首页之间的通信是最能体现Provider价值的地方。打开详情页时很多人习惯把当前收藏状态作为参数传进去然后在详情页改完后再通过Navigator.pop(true/false)把结果传回来。这个方案在只有一个入口时还能用但一旦首页、搜索页甚至推荐tab都能进入详情页你就得给每个入口加回调代码会变得极其啰嗦。用共用Provider就省心多了。详情页进入时直接用context.readFavoriteModel()获取同一个实例点击收藏后全局状态已经变了。返回首页时因为首页的收藏按钮还挂在Consumer下面它会自动感知新状态并刷新。这种方式既不需要埋回调也不需要处理页面返回时的多余逻辑数据流是单向的页面操作 - 状态更新 - UI自动响应。详情页的收藏按钮我也用了同一个FavoriteModel方法只是UI上略有变化。我加了一个小小的动画切换图标这里就不贴完整代码了核心是确保详情页和首页共用的是同一个Provider实例而不是在详情页里重新创建ChangeNotifier。这是很多状态不同步问题的根源。4.3 “我的收藏”列表页与空态设计独立收藏页是用户管理收藏的入口。我在导航栏上把它设计成一个Tab页默认展示收藏列表。数据源直接来自FavoriteModel.favorites。页面内部用AnimatedBuilder或ConsumerFavoriteModel来监听列表变化当favorites为空时显示空态一个大图标加一行提示文字“还没有收藏任何垃圾快去添加吧”。很多新手会忽略空态实际用户第一次进入页面时如果直接看到一片空白会以为App坏了。空态既是一种引导也是一种状态反馈成本不高但价值很大。有数据时我用ListView.builder渲染收藏卡片每个卡片都用Dismissible包裹支持左滑删除。删除回调里调用favoriteModel.removeFavorite(item.id)同时弹一个SnackBar提醒“已取消收藏”还可以加一个撤销操作。这个SnackBar的撤销功能其实就是调addFavorite因为Provider是全局单例能瞬间把数据加回去。这里有个Dismissible的使用细节一定要记住onDismissed回调触发后必须同步更新数据源让Dismissible对应的条目从列表里消失。如果你在onDismissed里什么都没做或者只是异步操作Flutter会报一个非常经典的错误“A dismissed Dismissible widget is still part of the tree”。我这个App里也踩过一次原因是我在删除前弹了个确认对话框没等对话框结束就返回了导致界面处于不一致状态。解决办法是先删数据、再提示不要把异步对话框夹在中间。4.4 组件通信选型思考写到这里顺便聊聊组件通信选型。很多Flutter新手分不清什么时候用回调、什么时候用Provider、什么时候用EventBus。我的经验是先看数据流向。如果只是儿子告诉父亲比如“我的收藏页点击了清空按钮通知首页刷新”用回调最简单。如果数据需要在多个页面之间共享并且不同页面都要修改同一份数据回调会传导得非常痛苦这种场景直接用Provider。EventBus这种东西我建议能不用就不用。虽然它能解耦复杂事件但也会让数据流变得难以追踪一旦出问题“谁发出了这个事件”非常难查。收藏场景下全局共享状态就是“收藏列表”它天然应该是一个单一数据源用Provider管理已经是足够的。与其引入EventBus增加心智负担不如把状态封装好。这是我在重构一次后得出的结论。5. OpenHarmony适配实录与避坑5.1 插件适配OpenHarmony需要对应的原生实现如果只做Android/iOSFlutter插件生态足够丰富pub.dev上随便找了个包就能用。但到了OpenHarmony事情就变了。很多插件依赖Android系统API或iOS的UIKit在OpenHarmony上没有对应实现直接调用会抛MissingPluginException或者PlatformException。我在项目里遇到的第一个插件坑就是shared_preferences。官方包默认不带ohos实现启动时一调就崩。当时查了半天才找到正确姿势要么用shared_preferences_ohos这个独立包要么使用OpenHarmony官方维护的flutter_packages仓库把shared_preferences替换为适配版。我选择了后者好处是API保持原样后续切回官方包时不需要改业务代码。shared_preferences: git: url: https://gitee.com/openharmony-sig/flutter_packages.git path: packages/shared_preferences/shared_preferences这里要特别提醒不要遇到问题再一个插件一个插件地换。在项目开始前先做一个“插件兼容性清单”。哪些插件用于UI纯Dart哪些插件触及原生能力哪些有ohos替代包全部列出来。这样能省很多时间。纯Dart的包比如dio、json_annotation通常没问题有原生channel的包都要逐个确认。5.2 存储路径与文件访问差异收藏功能里我把SharedPreferences当作唯一持久化手段但在OpenHarmony上它的行为跟Android不完全一样。底层实现不同导致某些API返回的路径、缓存目录和沙箱目录都有差异。如果使用path_provider标准版本在OpenHarmony上获取不到正确的getApplicationDocumentsDirectory需要换成path_provider_ohos才能拿到实际的沙箱路径。这种差异不是靠“改一个包名”就能解决的还有时序问题。Android的SharedPreferences实例化后基本可以同步读取但OpenHarmony上首次初始化可能要做一些底层的异步绑定立刻getInstance().getString()有概率拿到空值。我后来在启动流程里做了一个显示加载状态的过渡确保收藏页不会在冷启动时闪一下“空列表”。这一点很影响观感但也很好解决。另外在OpenHarmony上调试时不要依赖文件管理器的绝对路径。设备上的沙箱目录每次安装都可能变化在代码里写死路径换一台设备就崩。正确做法永远是走path_provider或shared_preferences这类抽象层。5.3 常见E/flutter崩溃和热重载问题我在前面提到过[ERROR:flutter/runtime/dart_vm_initializer.cc(41)] Unhandled exception这类日志在OpenHarmony调试中很常见。我的排障套路是先看异常类型再看有没有PlatformException字样最后定位到具体插件。如果是MissingPluginException基本就是某个原生channel没有相应实现如果是其它Dart异常那就正常按照堆栈查。热重载方面OpenHarmony的Flutter支持程度还算不错但有个前提你修改的必须是Dart代码。如果你动了ohos目录下的原生配置文件或者GPU相关代码最好重新执行flutter run不要依赖热重载。我遇到过热重载后状态丢失、页面卡死的情况并不是代码错误而是引擎在动态链接时没有正确更新原生部分。还有一次让我印象特别深我只是在onPressed里加了一个print语句结果热重载之后App直接闪退控制台也没输出任何有效堆栈。后来我清理了build目录重新全量构建才恢复。所以如果热重载出现诡异问题不要死磕直接重启项目往往更快。5.4 上架前的XTS自检要点如果开发完成后要上架到OpenHarmony应用市场XTS认证是躲不开的一环。这个认证主要是验证应用的稳定性、安全性和兼容性。在收藏模块上最容易影响XTS的是权限问题。我们的App本身不需要敏感权限但某些第三方库会在module.json5里声明额外权限导致认证时被质疑“申请了未使用的权限”。建议在发布前做一次完整的权限审查把module.json5里所有权限列出来逐条确认是否有实际调用。没有用到的权限全部删掉。另外签名配置也要提前做好XTS测试时会校验签名如果签名不正确部分功能无法正常测试。不要等到最后才去配签名不然可能在联调阶段就觉得一切正常、上架前才发现签名不匹配白白浪费一周时间。6. 问题排查与独家心得6.1 高频问题速查表实操过程中最耽误时间的都是细枝末节。我把收藏模块和OpenHarmony适配里的高频问题总结成一张表方便你对照自查。这张表不覆盖所有情况但覆盖了我本人遇到过的绝大多数坑现象可能原因解决思路flutter create后没有ohos目录Flutter版本非ohos分支切换为OpenHarmony SIG维护的分支启动即报MissingPluginException插件未适配OpenHarmony换成_ohos后缀的包或fork包Provider报“Could not find ChangeNotifierProvider”Provider没有在MaterialApp上层创建在runApp最外层包裹ProviderSharedPreferences冷启动读不到数据异步初始化未完成显示加载中await完成后再刷新收藏页删除后列表报Dismissible错误onDismissed未同步移除数据在回调里立即调用remove再notify返回首页后收藏状态没变化页面内新建了Provider实例删除子页面的Provider共享全局实例热重载后页面白屏原生配置或缓存不一致全量重新flutter run清理build目录6.2 一次“收藏状态不同步”的排查实录这个案例我印象特别深刻。当时在开发环境里收藏页功能一切正常首页也能显示收藏状态。但到了详情页点击收藏按钮后返回到首页发现首页的星标仍然没有变化。第一反应是Provider可能在不同页面创建了不同实例我去代码里找发现详情页在Navigator.push之前确实包了一个新的ChangeNotifierProvider当时是想给详情页传入一个独立的FavoriteModel结果反而把全局状态切断了。这个错其实很典型。很多Flutter新手为了图方便在页面内部再包一层Provider导致同一个业务状态被复制成了两个实例。修复方式很简单把详情页新创建的Provider删掉保留全局唯一实例在详情页里直接用context.readFavoriteModel()读取和修改状态。改完之后详情页返回首页星标立即刷新整个操作链变得异常顺滑。这次的教训是Provider实例的创建位置一定要规范全局共享状态就在顶层创建子页面只消费不再创建。一旦出现状态不同步优先检查是不是“重复实例”的问题比挨个页面加日志快得多。6.3 给后来者的几条建议做这个收藏功能前前后后折腾了两周我个人的体会是越“简单”的功能越考验工程基础。收藏功能虽然只是几十行逻辑但牵涉状态管理、本地存储、组件通信、平台适配几乎每种问题都能在里面找到影子。给正在做Flutter for OpenHarmony的同学三个建议。第一数据模型、存储逻辑和状态管理一定要拆开。我刚开发时图省事把收藏逻辑直接写在首页Widget里后面加“我的收藏”页时差点重构掉一层皮。单独封装成FavoriteModel后页面只管触发状态和存储都交给这一层代码立刻清爽了。第二插件优先级提前调查。不要等跑到某个功能才发现没有OpenHarmony适配提前列好清单比遇到一个大坑再去找解决方案快得多。第三多测极端操作。收藏场景最容易出问题的不是正常流程而是“快速连续点击”“重复收藏同一项”这种操作。写几个自动化用例或者手动反复点几遍能把隐藏的bug挖出来不少。如果你也在做类似项目建议把收藏这条业务线当成检验Flutter功底的试金石。它不难但每走一遍都能学到东西。把状态管理、持久化和跨平台适配这三大块真正吃透再去做别的功能心里会踏实很多。
返回列表