news 2026/7/18 6:02:19

07-Flutter 鸿蒙实战 07:故事集列表与缓存设计

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
07-Flutter 鸿蒙实战 07:故事集列表与缓存设计

07. Flutter 鸿蒙实战 07:故事集列表与缓存设计

用 Repository 缓存故事、照片、视频路径,减少重复请求和页面等待。

故事模块的特点

故事模块不是普通列表。每个故事可能关联照片、人物、地点、故事正文、语音、活化视频。用户从列表进入详情时,如果所有数据都等详情页再请求,就会先看到空白或 loading。这个项目用StoriesRepository做了一层缓存和预加载。

缓存字段集中在lib/features/stories/stories_repository.dart

staticList<Story>?_storyCache;staticfinalMap<String,Story>_storyDetailCache={};staticfinalMap<String,Uint8List>_photoCache={};staticfinalMap<String,Future<Uint8List?>>_photoRequests={};staticfinalMap<String,StoryPhotoInfo>_photoInfoCache={};staticfinalMap<String,Future<StoryPhotoInfo?>>_photoInfoRequests={};staticfinalMap<String,String>_videoFileCache={};staticfinalMap<String,Future<String?>>_videoFileRequests={};

这里不仅缓存最终数据,还缓存正在进行的 Future。这样同一张照片同时被多个组件请求时,不会重复发多次网络请求。

列表缓存

故事列表请求逻辑很直接:

Future<List<Story>>list({bool forceRefresh=false})async{finalcached=_storyCache;if(!forceRefresh&&cached!=null){returnList.unmodifiable(cached);}finalrows=await_api.getJsonList('/api/stories');finalstories=rows.whereType<Map<String,dynamic>>().map(Story.fromJson).toList();_storyCache=stories;for(finalstoryinstories){_storyDetailCache[story.id]=story;}returnList.unmodifiable(stories);}

列表请求成功后顺手写入详情缓存。这样从列表点击进入详情时,详情页能先展示已有的故事数据,再后台刷新。

图片缓存

照片字节通过photo(photoId)获取:

Future<Uint8List?>photo(String?photoId)async{if(photoId==null||photoId.isEmpty)returnnull;finalcached=_photoCache[photoId];if(cached!=null)returncached;finalpending=_photoRequests[photoId];if(pending!=null)returnpending;finalrequest=_api.getBytes('/api/photos/$photoId/image').then((bytes){_photoCache[photoId]=bytes;returnbytes;}).whenComplete((){_photoRequests.remove(photoId);});_photoRequests[photoId]=request;returnrequest;}

这个写法解决了两个问题:已经加载过的照片直接返回;正在加载中的照片复用同一个 Future。列表里有多处显示同一张图时,网络压力会小很多。

进入详情前准备数据

prepareStoryDetail是这个模块里很关键的设计:

Future<StoryDetailBundle>prepareStoryDetail(Stringid,{bool forceRefresh=false,})async{finalstory=awaitget(id,forceRefresh:forceRefresh);finalinfo=awaitphotoInfo(story.photoId,forceRefresh:forceRefresh);varvideoFilePath=_cachedVideoForPhoto(info);if(videoFilePath==null&&info?.shouldLoadAnimatedVideo==true){try{videoFilePath=awaitvideoFileForPhoto(info);}catch(_){}}unawaited(photo(story.photoId));returnStoryDetailBundle(story:story,photoInfo:info,videoFilePath:videoFilePath,);}

这段代码把故事、照片信息、视频缓存路径准备好,再交给详情页。即使视频预取失败,也不会阻塞打开详情页。

缓存失效

登录、退出或切换用户时必须清缓存:

staticvoidclearCache(){_storyCache=null;_storyDetailCache.clear();_photoCache.clear();_photoRequests.clear();_photoInfoCache.clear();_photoInfoRequests.clear();_videoFileCache.clear();_videoFileRequests.clear();}

故事属于用户私有数据,也可能包含家人共享内容。缓存跨账号残留会产生严重的数据错乱。

缓存一致性补充

缓存更新比缓存读取更重要

很多项目只写“读取时优先缓存”,但忘记写“更新后维护缓存”。这个项目在创建、更新、删除故事时都会同步维护_storyCache_storyDetailCache

if(cached!=null){_storyCache=[for(finalitemincached)item.id==story.id?story:item,];}

这样用户从详情页保存后回到列表,列表能直接看到新内容,不必等下一次全量刷新。

正在请求的 Future 也要缓存

_photoRequests保存的是进行中的图片请求。列表滚动和组件重建可能让同一张图片被请求多次,如果只缓存最终 bytes,重复请求仍然会发生。缓存 Future 可以从源头减少并发重复请求。

列表缓存和详情预取

缓存要解决两个问题

故事列表的缓存不是为了炫技,而是解决两个实际问题:列表返回快、详情打开快。列表页拿到故事后,会顺手把每个故事写入详情缓存;进入详情时就能先展示已有数据,再后台刷新。

Future<List<Story>>list({bool forceRefresh=false})async{finalcached=_storyCache;if(!forceRefresh&&cached!=null){returnList.unmodifiable(cached);}finalrows=await_api.getJsonList('/api/stories');finalstories=rows.whereType<Map<String,dynamic>>().map(Story.fromJson).toList();_storyCache=stories;for(finalstoryinstories){_storyDetailCache[story.id]=story;}returnList.unmodifiable(stories);}

这里返回List.unmodifiable,避免页面拿到列表后直接改缓存对象。缓存层保持数据所有权,页面只负责展示和触发刷新。

并发请求也要缓存

照片缩略图请求很容易重复。列表里多个组件可能同时请求同一张照片,如果只缓存结果,不缓存正在进行的 Future,请求还没完成时仍然会发出多次网络请求。

finalpending=_photoRequests[photoId];if(pending!=null)returnpending;finalrequest=_api.getBytes('/api/photos/$photoId/image').then((bytes){_photoCache[photoId]=bytes;returnbytes;}).whenComplete((){_photoRequests.remove(photoId);});_photoRequests[photoId]=request;returnrequest;

这个写法能把同一时间的重复请求合并成一次。图片越多、列表越长,收益越明显。

更新缓存比读取缓存更关键

缓存最容易出问题的地方不是读取,而是数据变化后没有同步。这个项目在创建、更新、重新生成、删除故事时都会维护缓存。

Future<Story>update(Stringid,StoryEditDraftdraft)async{finalstory=Story.fromJson(await_api.patchJson('/api/stories/$id',draft.toJson()),);_storyDetailCache[story.id]=story;finalcached=_storyCache;if(cached!=null){_storyCache=[for(finalitemincached)item.id==story.id?story:item,];}returnstory;}

如果只改详情缓存,不改列表缓存,用户返回列表会看到旧标题;如果只改列表缓存,不改详情缓存,再次进入详情又会回到旧内容。两个缓存都要同步。

缓存失效时机

故事缓存至少在这些场景下需要失效或刷新:

登录用户变化:清空全部故事、照片、视频缓存。 上传新照片:重新拉取故事列表或照片列表。 编辑故事:更新列表缓存和详情缓存。 删除故事:从列表缓存移除,并删除详情缓存。 重新生成故事:用服务端返回的新内容覆盖旧缓存。

这个项目在AuthController里处理登录态变化,在StoriesRepository里处理故事数据变化,职责边界比较清楚。页面不需要知道缓存内部有几个 Map,只需要调用 Repository 提供的方法。

列表验证路径

故事列表模块可以按下面路径验证:

1. 首次进入故事列表,触发网络请求。 2. 进入详情后返回,列表不闪烁、不重复请求同一批数据。 3. 编辑故事标题或正文后返回列表,列表展示新内容。 4. 删除故事后返回列表,被删除项消失。 5. 退出登录再登录另一个账号,列表不显示旧账号缓存。

这组场景能覆盖缓存命中、缓存更新和缓存清理。缓存做得好,页面会更快;缓存边界没处理好,数据错乱比慢更麻烦。

缓存问题排查

缓存对象要有清晰所有权

StoriesRepository用静态字段保存故事、照片、视频缓存。这意味着缓存是跨页面共享的,不属于某一个页面实例。

staticList<Story>?_storyCache;staticfinalMap<String,Story>_storyDetailCache={};staticfinalMap<String,Uint8List>_photoCache={};staticfinalMap<String,Future<Uint8List?>>_photoRequests={};

页面销毁不代表缓存清空。这样从列表到详情、再返回列表时,体验更流畅。但它也要求登录态变化时必须主动清理缓存,否则不同用户之间会互相影响。

forceRefresh 的使用边界

缓存读取需要一个强制刷新入口。项目里的listget都支持forceRefresh,普通进入页面可以用缓存,下拉刷新或保存后校正则走强制刷新。

Future<Story>get(Stringid,{bool forceRefresh=false})async{finalcached=_storyDetailCache[id];if(!forceRefresh&&cached!=null)returncached;finalstory=Story.fromJson(await_api.getJson('/api/stories/$id'));_storyDetailCache[id]=story;returnstory;}

没有forceRefresh,页面要么永远使用旧缓存,要么完全不敢用缓存。有了这个参数,缓存和真实数据之间就有了可控同步点。

缓存命中也要避免可变引用泄露

列表缓存返回时使用不可修改列表,能避免页面直接改 Repository 内部数据。

returnList.unmodifiable(stories);

如果页面直接拿到内部 List 并执行remove,Repository 可能完全不知道缓存已经变了。不可变返回值能把修改入口收回到 Repository 方法里。

图片缓存失败策略

图片加载失败时,当前请求会从_photoRequests移除,不会缓存失败结果。下次进入页面仍然有机会重试。

}).whenComplete((){_photoRequests.remove(photoId);});

这比把失败结果永久缓存更合理。移动网络失败可能只是临时问题,下一次刷新应该允许重新请求。

缓存问题定位表

现象优先检查对应文件处理方向
返回列表后还是旧内容列表缓存是否更新stories_repository.dartupdate/regenerate 后同步_storyCache
详情打开慢是否预取详情和图片prepareStoryDetail先用列表缓存,再后台刷新
图片重复请求很多是否缓存 pending Future_photoRequests同一 photoId 复用同一个请求
退出账号后仍显示旧故事缓存是否清理auth_controller.dart登录态变化时调用clearCache()

缓存的核心不是“存起来”,而是“什么时候用、什么时候更新、什么时候清”。三件事缺一件,缓存都会变成 bug 来源。

缓存容量控制

当前缓存适合中小规模故事列表。如果照片数量继续增长,图片和视频缓存需要容量控制,否则内存和磁盘都会变大。

故事列表缓存:按账号清空 图片内存缓存:限制最大数量或最大字节数 视频文件缓存:按最近使用时间清理 pending 请求缓存:请求结束后立即移除

故事文本可以保留久一点,图片和视频更适合做可回收缓存。缓存策略按资源成本区分,既能保证体验,也能避免 App 长时间使用后占用过大。
clearCache()|

缓存的核心不是“存起来”,而是“什么时候用、什么时候更新、什么时候清”。三件事缺一件,缓存都会变成 bug 来源。

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

Python高级编程核心技术:并发、内存管理与性能优化实战

Python作为一门功能强大且应用广泛的编程语言&#xff0c;在进阶阶段需要掌握更多核心技术来应对复杂场景。本文将从实际开发需求出发&#xff0c;重点讲解Python高级编程中的关键技术和实践方法。对于已经掌握Python基础语法的开发者来说&#xff0c;进阶阶段需要关注的核心能…

作者头像 李华
网站建设 2026/7/18 5:54:41

芯片ESD防护与系统级设计的关键差异解析

1. 芯片ESD防护能力与产品测试标准的差距解析当芯片规格书标注"ESD防护2000V"而产品测试要求达到8kV时&#xff0c;这个看似简单的数字差异背后隐藏着复杂的工程考量。作为从业十余年的硬件工程师&#xff0c;我经常遇到客户对此产生的困惑。首先要明确的是&#xff…

作者头像 李华
网站建设 2026/7/18 5:54:19

VoxelNeXt与ROS Noetic集成:实现实时3D目标检测的工程实践

1. 项目概述&#xff1a;当VoxelNeXt遇上ROS Noetic最近在折腾一个挺有意思的项目&#xff1a;把VoxelNeXt这个CVPR 2023上发表的先进3D目标检测模型&#xff0c;部署到ROS Noetic环境中&#xff0c;对nuScenes交通数据集进行实时检测。这听起来像是学术界和工业界的一次“联姻…

作者头像 李华
网站建设 2026/7/18 5:53:59

openclaw(小龙虾)

openclaw&#xff08;小龙虾&#xff09; 一、什么是openclaw openclaw是一个可以执行具体任务的AI智能体&#xff0c;让AI不仅仅停留在问答阶段&#xff0c;能真正帮助我们处理一些事情。 注意&#xff1a;小龙虾存在安全风险&#xff0c;请使用私人电脑进行操作。 需要nodejs…

作者头像 李华
网站建设 2026/7/18 5:53:29

深入解析Laravel ServiceProvider加载机制与优化实践

1. 为什么需要深入理解ServiceProvider加载机制作为Laravel框架的核心组件之一&#xff0c;ServiceProvider承担着服务注册与初始化的关键职责。很多开发者虽然每天都在使用各种ServiceProvider&#xff0c;但对底层加载机制却知之甚少。这就像每天都在使用手机却不知道它的工作…

作者头像 李华
网站建设 2026/7/18 5:52:45

AI Agent框架对比:Coze、Dify与LangChain技术解析

1. AI Agent 框架的崛起背景与核心价值2025年&#xff0c;全球AI Agent市场规模突破千亿美元&#xff0c;开发者面临的核心痛点从"如何调用大模型API"转变为"如何构建可持续演进的智能体系统"。在这个背景下&#xff0c;各类AI Agent框架应运而生&#xff…

作者头像 李华