做OpenHarmony上的Flutter应用是我最近一直在折腾的方向。手上正好有个美食烹饪助手的App在改版核心目标是把菜谱浏览体验做好分类、搜索、详情、收藏、历史记录一个都不能少。这篇文章不打算铺开讲所有功能单独拎出浏览历史记录这个模块从需求分析、数据模型、存储方案到UI实现、性能优化完整走一遍。如果你正好在用Flutter适配OpenHarmony或者在做任何App的浏览历史功能这篇应该能帮你少踩几个坑。1. 项目背景与技术选型为什么做这个组合1.1 美食烹饪助手的功能版图美食烹饪助手App一句话说就是给做饭的人提供菜谱全流程支持。用户在App里可以做三件事找菜、做菜、存菜。找菜靠的是分类浏览和搜索做菜靠的是步骤详情和计时提醒存菜则是把常做的菜加入收藏。这些功能听起来都不复杂但它们的共同基础是一套完整的浏览体系——用户在找菜和看菜谱的过程中产生的浏览行为恰恰是最有价值的数据。浏览历史记录在这个版图里承担的角色很容易被低估。用户昨天看了一道红烧肉的完整做法今天打开App想再确认一下配料比例如果找不到那条记录他要么重新搜索要么退出App去别处找流失率一下子就上去了。尤其对烹饪类应用来说用户反复查看同一个菜谱的频率比其他内容类App高得多因为做饭不是一次性的真正好的菜谱会被反复使用。这也是为什么我把这个模块放在和收藏同等重要的位置。这个项目里我规划的功能模块大概是菜谱广场推荐流、食材分类、全局搜索、菜谱详情、收藏夹、浏览历史、个人中心。浏览历史是个人中心里的一个一级入口后续还会基于它做猜你想做的推荐位。听起来功能面挺大但实际上每个模块都有独立的难点浏览历史算是其中经典到不能再经典的一个数据量可控但逻辑链条完整非常适合用来检验一个跨端框架在非标准平台上的真实战斗力。1.2 Flutter跨端方案在OpenHarmony上的落地现状说回技术选型。美食烹饪助手是跨端产品Android和iOS必须同时覆盖现在又多了一个OpenHarmony的要求。三个平台如果各写一套原生代码维护成本直接翻倍所以跨端框架几乎是必然选择。Flutter对OpenHarmony的适配和它对Android、iOS的官方支持不太一样。OpenHarmony这边主要是社区在推进具体来说是OpenHarmony SIG组织维护的flutter_flutter分支对应的是上游Flutter主仓库的某个稳定版本。这个分支通过OpenHarmony的ArkUI原生组件和Flutter引擎做桥接让Flutter渲染的UI能够以原生组件的形态嵌入到鸿蒙应用中同时保留Dart层的业务逻辑能力。实际体验下来基础Widget的渲染、常用Plugin的调用、热重载的开发流程基本都是可用的但细节上确实有些差异需要适配。当时做选型的时候我其实在Flutter和React Native之间纠结过。RN的生态更成熟一些但OpenHarmony这一侧的RN适配进度比Flutter要迟缓而Flutter的渲染引擎完全自绘跨端一致性理论上更有保障。加上团队里本来就有Dart基础迁移成本相对可控所以最终选了Flutter on OpenHarmony这条路线。你如果也要做类似选择我的建议是先确认目标设备上OpenHarmony的版本再对照社区flutter_flutter分支的适配说明看看你依赖的关键库有没有对应版本否则项目搭到一半发现某个插件没适配返工成本很高。1.3 为什么浏览历史记录值得单独拿出来讲很多人觉得浏览历史就是个列表数据库存一下查出来渲染完事。但真到线上你会发现它其实牵连着一大堆问题存储选型要考虑写入频率和查询效率去重策略影响用户感知的数据准确度长列表的图片加载会影响滚动流畅度加上OpenHarmony本身不是Flutter的首选平台第三方存储库的适配状态也参差不齐。所以我特别建议把这个小而全的模块当成一个练手项目来拆解。它的业务边界足够清晰不牵扯推荐算法和复杂交互技术链条又足够完整从Dart数据模型、JSON序列化、本地文件读写、状态管理到UI列表渲染全覆盖。对一个刚开始接触Flutter for OpenHarmony的开发者来说把历史记录完整做一遍比跟着官方文档建十个空项目都有用。2. 浏览历史的需求分析与数据模型设计2.1 两种常见的历史记录形态动手之前先把产品形态定下来。浏览历史在主流App里大概有两种呈现方式一种是今日头条、知乎那种按浏览时间倒序的时间线流每条记录里混着内容卡片你只能看到内容本身看不到明确的时间分组另一种是电商、视频App常用的日期分组列表记录按照今天的、昨天的、更早的维度折叠展示用户可以快速定位到某一天看过什么。对美食烹饪助手来说第二种明显更合适。原因是用户使用历史记录的场景往往是我记得昨天看到过一道菜想找出来——他有明确的时间线索按日期分组能让他更快找到目标。而且菜谱内容天然适合列表卡片展示封面图、标题、热量标识、标签一屏能放五条左右扫一眼就能认出是哪道菜。我在设计时选了日期分组列表顶部加一个清空历史的按钮长按单条记录可以删除交互路径尽量缩短。2.2 数据结构与冗余字段设计数据模型是这部分的重点。我先说结论历史记录表或者记录对象一定要冗余存标题、封面图和分类而不是只存一个菜谱ID然后每次实时去查。为什么因为菜谱的标题、封面、分类在运营后台是可以被编辑的。你今天存了一个红烧肉的ID明天运营把这条菜谱的标题改成秘制红烧肉封面也换了如果你只存ID明天打开历史记录看到的其实是新标题新封面——用户会觉得这功能坏了我什么时候看过这个但如果你冗余存储了一份浏览时的标题和封面历史记录就凝固在用户看它的那一刻这才是历史应该有的质感。具体字段我设计了这些recipeId菜谱唯一ID、title菜谱标题、coverUrl封面图URL、category所属分类、viewTime浏览时间ISO8601字符串存储、sourcePage来源页面用于后续数据统计。其中sourcePage是我额外加的后面做猜你想做推荐时会用到比如从分类页进来的和从搜索进来的反映的意图强度完全不同。序列化我建议先用手写的fromJson/toJson不用急着上json_serializable。原因很简单字段就六七个手写也就十几行代码引入代码生成器要在pubspec里加依赖、跑build_runner在OpenHarmony的适配环境下还可能遇到代码生成版本不匹配的问题。等字段膨胀到20个以上再考虑上自动生成不迟。2.3 存储上限与清理策略历史记录不能无限存。存太多有两个问题一是本地文件越来越大每次读写全量数据的时间跟着涨二是用户记录里全是三个月前随手翻过的内容真正想看的新记录反而被挤到下面体验是负分的。我的做法是上限200条超过后从最旧的开始丢弃。200这个数字是拍脑袋拍出来的吗不是。按一个家庭用户一天浏览30个菜谱算200条大概覆盖一周的浏览量超过一周的记录对用户来说基本没有回溯价值反而是数据噪音。如果你做的是内容资讯类App这个上限可以放宽到500甚至1000因为用户的浏览行为更频繁且时间线价值更高但如果做的是工具类App50到100条就够。清理策略上我做了一个双通道用户主动清空是主要的菜谱被运营下架时也会触发一条失效记录清理把历史记录里对应ID的数据删掉避免用户点击一条历史记录跳转后发现详情页是空的然后一脸懵。这个细节很多团队会漏掉但对内容类产品来说必须处理不然死链式体验会显著拉低用户好感度。3. 存储层实现本地持久化选型与落地3.1 三种存储方案的对比历史记录怎么存我列了三条路shared_preferences、sqflite数据库、JSON文件。shared_preferences对应OpenHarmony上是Preferences本质是一个键值存储适合存用户偏好设置之类的小数据。历史记录200条撑死也就几百KB存KV里听上去可行但KV存储没有查询能力每次取出来都要全量转成对象数组再在内存里排序去重逻辑复杂度不低而且频繁写入KV对性能是个负担实际体验下来并发读写时会偶发丢数据。sqflite是Flutter常用的SQLite插件支持结构化查询去重、排序、分页一句SQL搞定性能也够。但我查了一圈sqflite在OpenHarmony上的适配版本和各API的可用性在不同设备上有差异需要花时间验证对MVP阶段来说是个不确定性因素。JSON文件是我最终的选择。历史记录数据量小、结构简单、访问模式就是全量读全量写用文件存储天然匹配。每次浏览新增一条记录时读文件、解析、去重、插入、截断、写回整个过程在100条量级下耗时基本在10毫秒以内完全感知不到。等到数据量真的到了1000条以上再平滑迁移到sqflite也不迟。三种方案对比下来做了一张表我在内部文档里用的放到这里供参考方案优点缺点适用场景shared_preferences接入简单API友好无查询能力并发写入有风险偏好设置、tokensqflite查询能力强性能稳定OpenHarmony适配需验证数据量上千、查询复杂JSON文件简单可控零第三方依赖大数据量读写慢数据量数百级的小列表3.2 文件存储方案的具体实现确定方案后我建了一个HistoryRepository类统一封装历史记录的读写UI层完全不感知存储细节。这样做的好处是以后从文件换到sqflite只需要改这一个类页面代码一行不用动。核心代码大致长这样class HistoryRecord { final String recipeId; final String title; final String coverUrl; final String category; final DateTime viewTime; HistoryRecord({ required this.recipeId, required this.title, required this.coverUrl, required this.category, required this.viewTime, }); factory HistoryRecord.fromJson(MapString, dynamic json) { return HistoryRecord( recipeId: json[recipeId] as String, title: json[title] as String, coverUrl: json[coverUrl] as String, category: json[category] as String, viewTime: DateTime.parse(json[viewTime] as String), ); } MapString, dynamic toJson() { return { recipeId: recipeId, title: title, coverUrl: coverUrl, category: category, viewTime: viewTime.toIso8601String(), }; } }存储类class HistoryRepository { static const String _fileName browse_history.json; static const int maxCount 200; FutureListHistoryRecord loadHistory() async { try { final directory await getApplicationSupportDirectory(); final file File(${directory.path}/$_fileName); if (!await file.exists()) return []; final content await file.readAsString(); final list jsonDecode(content) as Listdynamic; return list .map((e) HistoryRecord.fromJson(e as MapString, dynamic)) .toList(); } catch (e) { // 文件损坏或解析失败时返回空列表宁可丢失历史也不能让App崩溃 return []; } } Futurevoid saveRecords(ListHistoryRecord records) async { final directory await getApplicationSupportDirectory(); final file File(${directory.path}/$_fileName); final content jsonEncode(records.map((e) e.toJson()).toList()); await file.writeAsString(content, flush: true); } }这里有个关键点文件路径用的是getApplicationSupportDirectory而不是getDocumentsDirectory或getTemporaryDirectory。原因是应用支持目录专门存应用内部数据系统备份时会被包含但不会暴露给用户直接浏览同时在OpenHarmony上这个路径的权限限制也是最小的。getTemporaryDirectory虽然读写方便但系统随时可能清掉历史记录这种需要持久保存的数据不能放那里。3.3 sqflite方案作为备选JSON文件方案虽然简单但如果你预判历史记录的量级会快速增长或者后续要做基于浏览历史的复杂联合查询比如最近三天浏览过的、分类为家常菜的菜谱那就应该直接上sqflite。我把这个方案也落实了一下作为备选。sqflite在OpenHarmony上的使用方式和Android上逻辑上是一致的。先是建表CREATE TABLE browse_history ( recipe_id TEXT PRIMARY KEY, title TEXT NOT NULL, cover_url TEXT, category TEXT, view_time TEXT NOT NULL, source_page TEXT );把recipe_id设为主键之后去重逻辑直接从读全量数据手动去重变成了INSERT OR REPLACE。每一次浏览记录写入时数据库自动帮你去重如果同一个recipe_id已经存在新记录直接覆盖旧记录。这个特性是我当初想在文件方案里手动实现的事数据库方案天然就支持了。查询也舒服按浏览时间倒序取出前200条SELECT * FROM browse_history ORDER BY view_time DESC LIMIT 200;所以sqflite方案不是不行是它的优势在你数据量起来之后才体现得出来。MVP阶段我选了JSON文件核心是把不确定性因素从技术栈里剔出去——在OpenHarmony这种非主流的Flutter平台上能少依赖一个第三方库就少一个风险点。3.4 OpenHarmony环境下的路径与适配问题最后补充一下OpenHarmony上的适配问题。使用path_provider或它对应的OpenHarmony适配版本获取目录时返回的路径格式和Android上略有差异这很正常不用慌直接用就行。但有一个坑要提前避开不要在调试阶段直接往根目录或者外部存储写文件鸿蒙对应用沙箱的限制比Android要严格写不进去不说有时还会抛权限异常排查起来很费时间。我实际踩过的一个问题用path_provider获取目录时在Android设备上返回的是 /data/data/包名/files在OpenHarmony开发板上返回的路径风格完全不同。当时我写了个硬编码路径去读取历史记录文件结果在鸿蒙上读取到的永远是一个不存在的路径。后来我把所有存储路径改为统一走path_provider的方法之后问题就消失了。这个经验总结下来就是永远不要在代码里硬编码存储路径跨端平台差异比你想象的要多。另外一个关于目录选择的问题不要在文件读写时用path_provider获取目录然后手动创建子目录如果app支持目录里没有目标目录记得先创建。我在OpenHarmony的开发环境里遇到过File.writeAsString时目录不存在导致的异常后来加了一段ensure目录存在的逻辑才解决。4. 核心功能实现从埋点到列表展示4.1 浏览事件的采集时机与防抖先说采集时机。用户什么时候算浏览了一条菜谱我的定义是菜谱详情页成功加载且页面可见超过3秒。为什么不是进入页面就算因为用户可能点进来发现加载失败或者立刻退出这种记录写进历史里反而是噪音。3秒的阈值是个经验值后台可以做成可配置的字段没必要在客户端写死。实现上在菜谱详情页的initState里启动一个3秒的Timer页面正常展示且在这3秒内没有触发过手动退出就回调采集。用户如果在3秒内退出Timer要记得取消避免已经销毁的页面对象还在回调里触发写入那会引入一个很隐蔽的bug。再补一个防抖逻辑用户从列表页点击A菜谱返回又点击A菜谱这个过程中浏览记录应该只在第一次点击时写入或者只在最后一次退出时更新一下时间而不是每次进入都写一次。我用的方案是记录一个lastRecordedRecipeId和lastRecordedTime如果用户重新进入的菜谱和上一次记录的相同且间隔小于5分钟就不再重复写入只更新时间戳。5分钟这个值看场景烹饪类App里用户反复查看一个菜谱是高频行为间隔设长一点更合理。4.2 去重插入的核心逻辑去重插入是历史记录模块的核心逻辑。用户看过一道菜五次历史记录里应该只出现一条但浏览时间要更新为最后一次看的时间且这条记录要移到列表最前面。JSON文件方案的实现思路Futurevoid addRecord(HistoryRecord record) async { final records await loadHistory(); // 1. 去重移除同ID的旧记录 records.removeWhere((e) e.recipeId record.recipeId); // 2. 新记录插入头部 records.insert(0, record); // 3. 截断超过上限的丢弃 if (records.length maxCount) { records.removeRange(maxCount, records.length); } // 4. 写回 await saveRecords(records); }这四步放在一起就是一个完整的浏览记录更新事务。单独看每一步都没什么技术含量但组合起来的顺序是有讲究的先removeWhere再insert避免出现同一条记录在list里重复的情况先截断再写回保证文件里永远不会出现超过maxCount的数据。如果你是直接用sqflite同样逻辑就是一句INSERT OR REPLACE加一条DELETE语句思路一致。这里我再强调一个很多人会忽略的细节insert(0, record)之后再saveRecords如果UI正在展示历史列表需要通知列表刷新。这个通知不是自动发生的你必须主动触发状态更新否则用户看到的历史记录不会实时反映最新浏览结果。我在项目里通过状态管理层向历史列表页发送了一个数据已更新的事件页面收到后重新加载数据并setState。4.3 历史列表UI与分组展示历史列表页我用了ListView.builder。为什么不用ListView因为历史记录最大200条ListView.builder按需构建列表项滚动到哪条才构建哪条不会有一次性构建200个Item的开销。这对有封面图的列表尤其重要图片解码本身就很耗内存。分组逻辑上是把records按viewTime的日期聚合。具体做法遍历记录列表把同一天的记录归到一个桶里然后按日期倒序排列桶。组内记录按浏览时间倒序组与组之间按日期倒序。对应到UI上就是每一组前面加一个日期标签今天昨天2025年6月10日。日期的格式化我没有用第三方库手写了一个小函数逻辑很简单当天显示今天昨天显示昨天一周内显示周X再早显示YYYY年M月D日。用户最常用的场景基本都覆盖了。列表项的设计左边是菜谱封面图的缩略图右边是标题和分类标签右下角显示格式化后的浏览时间。封面图使用96x96的裁剪尺寸加载时指定cacheWidth参数让Flutter在解码时就缩小图片而不是加载原图再缩放这个对内存的优化效果非常明显。如果是远程图片记得用cached_network_image之类的缓存组件或者自己包一层内存缓存否则列表快速滑动时会反复触发网络请求卡顿和流量消耗都会上来。列表页我还接了一个下拉刷新虽然历史记录是本地数据理论上没有刷新需求但用户已经习惯了列表页下拉刷新的交互加上之后统一了手势习惯顺手还能把本地文件和内存状态做一次对账。4.4 删除与清空操作删除交互我做了两层单条删除用长按弹底部操作菜单里面是删除这条记录和取消清空则在页面顶部放一个按钮点击后弹出确认对话框确认后一次性清空。这里有个产品细节值得说一下清空操作的确认文案要具体。如果只写确定清空吗用户往往会犹豫我写的是将清空全部浏览记录确定继续吗同时标出具体数量共200条用户对后果的预期更明确误操作的几率也低。这是很小的优化但能让产品显得更成熟。技术上就是调用repository的clearAll方法然后刷新列表状态顺手把空态展示出来。4.5 空态与引导设计历史记录为空时页面如果直接显示一个白屏用户会觉得这个功能坏了。所以空态视图是必须的一张简单的插图加一句还没有浏览记录去发现美食吧加一个去逛逛按钮点击后跳转到菜谱广场。这个引导闭环能让用户从空态页无缝回到内容浏览而不是退出去重新找入口。5. 与App内其他模块的协作5.1 状态管理方案选型浏览历史模块不是孤岛它需要和菜谱详情页、广场列表页、个人中心联动。比如用户在详情页看了菜谱回到个人中心点进历史记录必须能看到刚才那条记录出现在最前面这要求跨页面共享数据状态。状态管理我最终用了Provider。原因还是那句能在OpenHarmony的Flutter环境里稳定跑起来的就是最简单的方案。Riverpod也不错但项目里现有代码都是Provider风格统一用一个省得两套心智模型切换。Bloc在这个模块里属于杀鸡用牛刀状态事件流的抽象本身会引入一大堆模板代码对历史记录这个业务来说过度设计。具体做法建一个HistoryProvider继承ChangeNotifier内部持有List 和加载状态。菜谱详情页浏览记录写入成功后调用provider.addRecord历史列表页监听provider的数据自动刷新个人中心页在显示历史记录入口的同时也展示最近浏览3条的预览数据源都是同一个provider。这样不同页面之间不需要传构造函数参数也不用依赖全局事件总线状态集中管理数据一致性自然就保证了。5.2 历史记录与收藏、搜索的联动历史记录和收藏是两个独立功能但它们的数据链路有联动的地方。我做了两个联动点第一菜谱详情页会显示当前菜谱是否已收藏、是否在浏览历史中的状态标识。这个数据在进入页面时从两个数据源分别查询。历史记录里点进来的详情页会带一个已在历史记录中的轻提示但不会像收藏那样变成按钮态因为历史记录是自动产生的不需要用户主动操作。第二搜索页的搜索提示词会结合浏览历史做个性化用户搜索时输入框下方展示最近浏览的菜谱作为快捷入口点击直接跳详情。这个功能对美食App非常实用因为用户来搜索往往是想找一个最近看过但没记住名字的菜把浏览历史直接摆到搜索页转化路径短得感人。实现上搜索页在初始化时通过provider读取最近10条历史记录渲染成一排横向滚动的小卡片。5.3 跨组件通知与数据刷新跨组件通知的需求出现在一个场景里用户从历史记录页删除了某条记录但菜谱详情页如果在后台栈里那条菜谱的已浏览状态标识还亮着。正常情况下这不影响使用因为用户回到详情页时页面会走生命周期回调。但有一点要处理好如果历史记录被清空了后台栈里的详情页再返回时可能会显示一个已经失效的浏览状态。具体到实现我用的是Provider的监听机制详情页监听HistoryProvider的变化当对应recipeId不存在时自动更新状态。这一块逻辑不复杂但漏掉会让用户觉得数据对不上属于典型的看着是小问题体验起来很别扭的类型。我建议做这类多页面数据联动时统一走数据层加状态管理的方案不要用事件总线或全局静态变量不然项目后期维护会很酸爽。5.4 并发写入的防护策略我遇到过一个很典型的并发问题用户在菜谱详情页快速跳转浏览A菜谱和B菜谱的浏览记录几乎同时触发写入。文件方案的写入过程是读-改-写两个写入请求并发执行时可能出现A读到了旧数据B也读到了旧数据A写回B也写回结果A的写入被B覆盖历史记录里只剩B。解决方案是引入一个简单的写入锁在repository里用一个Future队列所有写入操作排队执行同一个时刻只有一个写入操作在读写文件。Dart是单线程的async的Future本身不会并行执行但因为await的间隙两个操作确实可能交错。用锁把读-改-写变成原子操作后问题就解决了。代码不复杂一个标志位加一个等待循环就能搞定。6. 性能优化与问题排查实录6.1 高频写入导致卡顿的优化先说一个实际的卡顿场景用户在菜谱详情页快速连续滑动到下一个菜谱每滑一个就触发一次历史记录写入。JSON文件方案的写入是全量写200条即便不大但在弱性能设备上连续写也会出现掉帧。我的优化方案是给写入操作加队列合并。具体来说页面上的采集事件不打日志到文件而是先放进一个内存队列用一个500毫秒的定时器批量处理500毫秒内的所有浏览事件合并成一次文件写入。用户正常浏览速度下这个批量窗口基本不影响数据准确性但写入次数能降低80%以上卡顿感直接消失。这个思路类似于前端框架里的批量更新原理完全一样。6.2 图片加载与内存控制历史列表的图片优化前面简单提过这里展开说。菜谱封面图原图往往在1024x1024以上如果直接用Image.network加载每张图内存占用可能达到3MB以上列表滑过20张就是60MB美食App还有很多其他页面也在用图片内存很容易爆。我的做法是三层控制第一网络层请求图片时带上宽高参数让服务端返回WebP缩略图或者指定裁剪尺寸比如96x96或128x128第二Flutter侧解码时设置cacheWidth让图片在解码阶段就按目标尺寸解码不做多余的解码工作第三列表滑动时开启懒加载只有滚动停止后才加载可见区域附近的图片。这三层叠加内存占用能降到原来的1/10左右而且对OpenHarmony这种对内存管理比较敏感的平台来说这样做的意义更大。6.3 OpenHarmony适配的兼容性坑Flutter for OpenHarmony的适配毕竟不是官方主航道实际跑下来有几个坑值得记录第一个是PlatformView的差异。Flutter在OpenHarmony上通过ArkUI组件桥接原生视图如果你在历史列表页里嵌入了原生组件比如视频封面预览之类的滚动性能和事件透传都可能出现异常。我的建议是历史列表这种纯展示页面避免使用PlatformView全部用Flutter自绘组件性能最稳。第二个是字体渲染。部分OpenHarmony设备上中文字体的字重和字距表现和Android不太一样列表页里标题文字有时会出现轻微的重影或模糊。排查下来是字体渲染引擎的差异解决方案是给关键文字加上fontFamily指定一个系统自带的中文字体或者用fontWeight: FontWeight.w500替代默认的w400视觉上会好很多。第三个是onBackground生命周期事件。Flutter on OpenHarmony里App切换到后台时生命周期的回调时序和Android有差异如果你在didChangeAppLifecycleState里做了数据保存要留意时序问题别让后台上传和文件写入打架。我跑过一次历史记录在后台被错误覆盖的问题最后是在保存逻辑里加了一个互斥锁解决。6.4 典型报错速查表最后整理一张我开发过程中遇到过的报错速查表对应的都是真实场景报错/现象可能原因处理方案e/flutter (31173): [error:flutter/runtime/dart_vm_initializer.cc(41)] Unhandled Exception某个异步操作未捕获异常多半是文件读写或JSON解析在异常链路上加try/catchloadHistory返回空列表兜底历史记录清空后再进入App记录又出现清空时只清了内存没清文件或文件写入未flush用flush: true参数写入清空操作同步触发文件覆盖列表快速滚动时大量网络请求图片未走缓存组件换用cached_network_image或手动加内存缓存在OpenHarmony上读取不到历史文件硬编码了Android路径统一改用path_provider获取应用支持目录删除一条记录后界面闪一下但数据没变状态刷新后重新loadHistory覆盖了删除结果在数据写回后才刷新状态保证读取到的是最新文件就拿最后一行的删除记录后界面没变来说我调试了好久才发现是异步竞态问题删除操作还在写文件UI就触发了重新load读到的是删除前的旧数据界面自然没变化。后来我把删除操作封装成async方法UI等文件写入完成后再刷新列表问题彻底消失。这种异步竞态在文件存储方案里特别容易碰到写代码时多留意操作顺序能省很多排查时间。看到这里浏览历史这个模块从需求设计、数据模型、存储实现到UI交互、性能优化整个链条的每一步就都过完了。做这个小项目最大的体会是跨端开发的难点从来不在UI有多炫而在那些底层能力——数据怎么存、状态怎么同步、异常怎么兜——在非标准平台上的表现是否稳定。历史记录恰好把这个特点体现得淋漓尽致。我个人后续的扩展方向有两个一个是把浏览历史同步到云端做多端设备的历史延续换设备不丢记录另一个是基于浏览历史做菜谱推荐——用户过去一周总看川菜首页就多推川菜。这些扩展都依赖一个稳定可靠的历史记录底座所以把底座打扎实值。如果说最后要我分享一个技巧那就是优先保证写入不丢、读取不崩这两个底线再去追求UI上的精致模块的可靠性永远第一。
阅读完成 · 觉得有帮助?