ARTICLE DETAIL

资讯详情

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

Flutter on OpenHarmony菜谱详情页实战:从选型到性能优化全记录

Flutter on OpenHarmony菜谱详情页实战:从选型到性能优化全记录 刚把这个“随手小灶”App 的菜谱详情页从零跑通的时候我第一反应不是发朋友圈庆祝而是翻了一遍我在 OpenHarmony 社区群里攒了半年的聊天记录——说实话问“Flutter 能不能跑 OpenHarmony”的人越来越多但真正把完整项目流程分享出来的少之又少。这篇博文就想当那个“少之又少”。我会围绕最近做的一个美食烹饪助手 App重点拆解菜谱详情展示的实现过程包括技术选型、数据层设计、页面 UI 搭建、组件通信和真机调试全程用真实踩坑经验说话适合正在研究 Flutter for OpenHarmony 的移动端开发者、准备接入开源鸿蒙生态的团队以及想抄一套可用方案的独立开发者。1. 项目背景为什么是 Flutter OpenHarmony 做菜谱 App1.1 选型思考跨端成本与团队技能栈先说结论如果团队里没有专职的 OpenHarmony 原生开发又不想被 Android、iOS 两套代码拖死用 Flutter 做 OpenHarmony 应用是目前性价比最高的一条路。我当时接手这个美食项目时团队情况很现实两个后端转前端的同学Dart 基础不错但没人写过 Java 鸿蒙应用。一开始我还认真评估过用 ArkTS 重写一套毕竟 OpenHarmony 官方推荐的声明式 UI 是 ArkTS性能理论上最贴近系统底层。但看完详情页要做的功能列表我果断扔掉了这个念头——常规的列表、标签、轮播、弹出层ArkTS 都能做但团队学习成本摆在那里至少要多花两周时间把 ArkTS 的状态管理、组件生命周期、路由机制全部走通而 Flutter 这边大家本来就熟直接换一套编译目标而已。另一个决定性因素是 Flutter 的 UI 表达力。菜谱详情页这种内容是典型的重视觉场景封面大图、食材表格、步骤时间轴、计时器交互每一项都需要高频次调整布局。Flutter 的组件体系和 Hot Reload 在这种场景下的开发效率是碾压级的。我在项目里调一个步骤卡片的内边距改完代码直接热重载看效果整个调整过程不到 20 秒这在原生开发里很难做到。当然Flutter for OpenHarmony 不是没有代价。它本质上依赖 OpenHarmony-SIG 维护的 flutter_flutter 适配分支社区版本跟 Flutter 官方主线的版本更新会慢半拍遇到插件兼容问题也只能自己啃源码。一句话总结我的选型判断成熟团队要抢时间、要一套代码多端复用直接上 Flutter如果项目对系统 API 的依赖特别深比如要做摄像头采集、底层硬件控制那还是得老老实实评估 ArkTS 或混合开发。1.2 菜谱详情页的需求拆解说句实话菜谱 App 的功能看多了你会发现详情页才是决定用户留不留得住的命门。列表页大家做得都差不多但详情页一旦信息乱、加载慢、图片崩一顿饭的兴致直接没了。所以动手前我先把详情页的需求列成了清单避免写代码的时候被零散想法带偏。核心信息展示方面一道菜必须讲清楚这几个维度封面图、菜名、烹饪时长、难度、份量、主料/辅料清单、分步骤做法、小贴士。其中步骤部分不能只是一堆文字堆在那里我要做成时间轴样式步骤编号、操作说明、单步耗时、成品缩略图都要有。考虑到很多人做菜的时候手上全是油整个页面的按钮热区必须做得足够大文字也支持跟随系统字体缩放。交互功能方面详情页至少要承载收藏/取消收藏、分享、评分、烹饪计时器这几个动作。收藏状态需要跟收藏列表页保持同步计时器在用户退到后台继续计时这些都是容易踩坑的地方后面章节我会一个一个展开讲。数据与性能要求就更直接了首屏图片不能等全部加载完才显示文字内容要优先渲染滚动跟手不卡顿页面销毁不能崩溃。这些指标跟 Android 端调优起来其实没有本质区别但 OpenHarmony 设备型号杂、屏幕尺寸差异大适配范围要提前规划好。我后来把需求整理成表格挂在项目看板上每个功能点对应一个验收标准后面开发才没有返工。2. 工程搭建让 Flutter 在 OpenHarmony 上跑起来2.1 环境准备与版本对照这一节是给第一次接 OpenHarmony 的 Flutter 开发者看的“省流版”环境指南。我踩过的最大坑就是版本匹配所以先把版本对照表放出来你直接照着配能省一整天时间。我当时的本机环境是DevEco Studio 4.0 Release、OpenHarmony SDK 3.2 Release、Flutter 使用来自 OpenHarmony-SIG 的 flutter_flutter 仓库的openharmony-3.2-branch分支Dart 版本跟随该分支自带。这里特别强调一下千万别用官方 flutter 主干 SDK 去跑 OpenHarmony 编译会直接报错找不到 ohos 平台配置整个命令行只能干瞪眼。环境配置的步骤其实不复杂从 Gitee 克隆 OpenHarmony-SIG/flutter_flutter切换到对应分支把仓库里的flutter脚本路径加入 PATH并确认flutter doctor能识别 ohos 工具链安装 DevEco Studio配置好 OpenHarmony SDK 路径创建空的 OpenHarmony 工程后续 Flutter 产物会以模块方式塞进这个工程在项目里执行flutter create --platformsohos .生成 ohos 目录或者直接通过适配分支的 build 命令生成 hap 产物我在配置过程中第一次跑flutter doctor时工具链识别出了 Android SDK 但完全不认 ohos问题出在环境变量DEVECO_SDK_HOME没指向 OpenHarmony SDK 目录。这个环境变量是适配版 Flutter 特有的官方文档写得比较隐晦我折腾了快一个小时才定位到。配置完成后正常编译一次大概要两三分钟比 Android 首次跑 Gradle 快不少。提示版本匹配是 OpenHarmony 适配路上的头号坑。建议记录你的 OpenHarmony SDK API 版本和 Flutter 分支的对应关系别拿到什么版本都往上怼。2.2 工程结构设计与依赖引入工程结构我这里采用的是 feature-first 的模块化方式跟菜谱业务强相关的代码全部按功能拆包公用的组件和工具独立放一层。这样做的原因很简单详情页绝对不是孤立页面首页、分类、搜索都会跳过来以后还要加购物车、厨房笔记不把边界划清楚后面改一个入口就得全局翻代码。实际的目录结构长这样lib/ ├── main.dart ├── core/ │ ├── network/ │ ├── utils/ │ └── theme/ ├── features/ │ ├── home/ │ ├── recipe/ │ │ ├── models/ │ │ ├── providers/ │ │ ├── pages/ │ │ └── widgets/ │ ├── favorites/ │ └── settings/ └── shared/ └── widgets/依赖管理方面我核心就选了四个包provider做状态管理、cached_network_image做网络图片缓存、dio做网络请求、intl做时间格式化。像国际化、手势缩放这类需求能不用第三方就尽量不用因为在 OpenHarmony 上不是所有 Flutter 插件都有对应实现。我试过一个挺流行的毛玻璃效果插件Android 端正常但里面依赖了 Android 特有的PlatformView逻辑跑到 OpenHarmony 上直接编译不过最后只能自己用半透明遮罩模拟。所以依赖引入原则就一句话能用官方组件解决的绝不多引包多一个插件就多一个适配风险点。2.3 接入 OpenHarmony 工程的两种方式跑通 Flutter 和 OpenHarmony 的工程联调我试过两条路各有优劣。第一种方式是把 Flutter 作为独立模块嵌入 OpenHarmony 工程。先创建 OpenHarmony 原生工程再把 Flutter 生成的 ohos 目录作为模块引入通过 DevEco Studio 统一编译和运行。这种方式的优点是调试链路完整DevEco 的日志、断点、性能分析工具都能直接用真机部署也方便。缺点是每次 Flutter 代码改动后要先构建产物再回到 DevEco 里刷新工程双工具切换比较烦。第二种方式是用适配版 Flutter 直接构建 hap 包然后通过命令行工具部署到设备或模拟器。这种方式更接近 Flutter Android 的体验命令行一条龙很快但日志查看就不如在 IDE 里方便出错信息也要自己捞。我的实际建议是开发阶段用第一种方式跑通主流程到了 UI 调优阶段切到第二种方式用 Hot Reload 快速调样式上机自测回归再回到第一种方式毕竟 DevEco 的调试体验最稳。这个工作流相当于把两种方式都用上虽然听起来有点折腾但实际用下来效率最高。注意在 OpenHarmony 工程里引入 Flutter 模块后记得检查module.json5里有没有申请必要的权限。菜谱 App 如果不涉及网络图片之外的能力一般只需要网络权限但如果你做分享功能想截屏就需要额外申请媒体权限漏掉的话运行时直接抛异常。3. 数据层与状态管理菜谱数据怎么组织最舒服3.1 菜谱数据模型设计数据模型是整个详情页的地基。我的原则是前端模型只放 UI 展示需要的数据网络返回的额外字段能扔就扔避免把 JSON 原封不动塞进状态里后面每个页面都要为没用的字段负责。Recipe模型我设计了五个核心字段组对应详情页的五大区块基本信息id、封面图 URL、标题、简介、烹饪参数时长、难度、份量、食材清单主料和辅料分开存储、步骤列表有序数组每个步骤包含序号、说明、预计耗时、可选成品图 URL、补充信息小贴士、热量标签。模型实现用的是手写fromJson/toJson没用 json_serializable 生成器。这个模型设计的关键点有两个。第一食材清单里的每一条除了名称和数量我还存了一个isKeyIngredient标志位用来在主料区做高亮展示这个字段后端接口本来没有是前端根据食材重要性规则算出来的。第二步骤数组使用普通ListStep而不是 Map因为步骤是有强顺序的数组天然保证顺序也方便后面做步骤编号、计时、跳转定位。对应 JSON 大概长这样{ id: RECIPE_001, title: 番茄炖牛腩, coverUrl: https://.../cover.jpg, durationMin: 90, difficulty: medium, servings: 3-4人份, ingredients: [ {name: 牛腩, amount: 800克, isKeyIngredient: true}, {name: 番茄, amount: 4个, isKeyIngredient: true}, {name: 洋葱, amount: 1个, isKeyIngredient: false} ], steps: [ {seq: 1, text: 牛腩冷水下锅焯水, durationMin: 5, imageUrl: null}, {seq: 2, text: 番茄去皮切块, durationMin: 10, imageUrl: null} ], tips: 番茄炒出沙后加牛腩汤汁更浓郁 }3.2 列表到详情的导航传参直接传对象还是传 id这算是我最想分享的一个设计决策。网上很多 Flutter 教程写详情页跳转都是Navigator.push直接把列表页的整个对象传过去看起来很方便但生产环境里这么做会埋一个隐患——详情页拿到的是列表页的静态快照一旦数据在服务端更新或者用户从收藏列表、搜索历史多个入口进来看到的详情内容可能就不是最新的。我最后选择的是“只传 id详情页自己拉数据”的方案。列表页跳转时只把recipeId通过构造参数传过去详情页的Provider从仓库层按 id 拉取完整数据并缓存。这样不管用户从哪个入口进来详情页都是同一套数据加载逻辑后续做下拉刷新、收藏状态同步也顺理成章。代价是详情页多了一次网络加载状态的处理。为此我设计了loading、loaded、error三种状态用Provider里的一个枚举统一管理页面根据状态决定是显示骨架屏还是错误重试按钮。首访问时会有几百毫秒的白屏我用骨架屏撑住了体验比直接传对象省下来的维护成本划算得多。经验如果你做的是纯前端 Demo直接传对象没毛病代码还简单但项目要长期维护、多入口跳转传 id 拉数据才是正解。这个取舍越早做越好后期改成本很高。3.3 Provider 管理详情页状态的实战姿势项目里状态管理我选了provider而不是 getx、riverpod 这些原因很实际团队最熟、社区资料最多、OpenHarmony 适配版本上没有任何 feature 需要特殊处理。菜谱详情页的状态不算复杂用ChangeNotifier完全够用。详情页的 Provider 我是这样设计的RecipeDetailProvider负责承载当前菜谱数据、加载状态、步骤完成的标记、收藏状态、评分FavoritesProvider负责维护整个 App 的收藏列表 id 集合。两个 Provider 的分层逻辑很明确——详情页自己的临时状态用局部 Provider跨页面要共享的收藏状态用全局 Provider。Provider 的使用就两个关键点少放全局、分层清晰。详情页内部的加载状态、评分草稿这类东西一辈子跟页面共存亡放进全局只是白白增加重建范围收藏状态因为收藏列表页、详情页、首页卡片都要读就必须全局单例。什么时候Provider.ofT(context)会导致 widget 重建只要你监听的那个值变了所有依赖它的 widget 都会通过Consumer或context.watch触发重建。所以我把详情页拆成了多个小的Consumer比如仅评分组件监听评分值仅收藏按钮监听收藏状态这样改一个局部状态不会让整棵页面子树全部重建滚动性能也好。具体代码结构大概是class RecipeDetailProvider extends ChangeNotifier { RecipeDetailState _state RecipeDetailState.loading; Recipe? _recipe; final Setint _completedSteps {}; double _rating 0.0; bool _isFavorite false; final FoodRepository _repository; Futurevoid loadRecipe(String id) async { _state RecipeDetailState.loading; notifyListeners(); try { _recipe await _repository.fetchRecipeById(id); _state RecipeDetailState.loaded; notifyListeners(); } catch (e) { _state RecipeDetailState.error; notifyListeners(); } } }页面里的订阅代码不需要太花哨ConsumerRecipeDetailProvider包住对应区域就够了。后面收藏列表要跟详情页同步我也用的同一个全局 Provider 实例在详情页写收藏操作收藏页通过context.watchFavoritesProvider()自动刷新这是我目前觉得最舒服的写法。4. 详情页 UI 构建实录4.1 骨架搭建SliverAppBar CustomScrollView详情页的骨架我放弃了传统的Scaffold SingleChildScrollView Column组合改用了CustomScrollView SliverAppBar SliverToBoxAdapter。原因很简单封面大图要能展开收起滚动时标题栏要渐变吸顶这套交互动效用普通 Column 根本做不优雅。SliverAppBar给整个页面带来的手感提升是非常直观的。初始状态下封面图完整露出标题悬浮在图上往下滚动时封面图跟随收起最终变形成一个半透明导航栏露出 “番茄炖牛腩” 的菜名。细节处理上flexibleSpace区域我放的不只是封面图还在底部压了一层从透明到黑色的渐变遮罩保证白色文字在任何背景图上都能看清。页面剩余内容放在了SliverToBoxAdapter里内部再按区块拆成独立的 widget。信息区、食材区、步骤区、小贴士区各自独立好处有两个一是代码好维护改一个区块不会碰其他区块二是配合下一节说的性能优化可以按区块做局部刷新避免整页失效重绘。有一点要提醒SliverAppBar在 OpenHarmony 设备上如果pinned设为 true吸顶时 subtitle 和背景渐变色的衔接偶尔会出现 1px 的视觉缝隙。我实测下来给导航栏背景加一个跟页面底色一致的 0.5 高度Container兜底就能解决这种小瑕疵通常只在真机上暴露模拟器上很难发现。4.2 食材清单与步骤时间轴实现食材清单在视觉上是详情页最容易做得难看的部分。我见过很多 App 直接把后端返回的数组渲染成一长列主料辅料混在一起用户得自己数着哪一行是主料。我的做法是分成“主料”和“辅料”两张卡片主料卡片做浅色背景每行用Row左对齐辅料卡片用紧凑的双列布局节省垂直空间。食材项的Row左侧放食材名右侧放用量中间用虚线分隔线撑开视觉上很干净。这里有一个我研究过的小细节用量文本我用的TextOverflow.ellipsis但实际菜谱数据里“800 克”“3-4 人份”这种字段不会太长真正会溢出的情况发生在系统字体放大 1.5 倍以上所以这个兜底一定要加不然无障碍用户一放大字体页面就直接炸布局。步骤时间轴是我的得意之选。每个步骤生成一个垂直条目左侧是圆形序号带一条贯穿的虚线连到下一步中间是操作说明文本右侧是预计耗时用浅色标签显示“约 5 分钟”。用户点一下步骤序号圆点会变成高亮色并打上完成标记。同时圆形序号支持点击跳转定位如果步骤附带了过程图点击缩略图还能放大预览。时间轴的实现不复杂就是Column里依次叠加Row背景用CustomPaint画一条虚线但视觉效果比普通列表好了不止一个档次。4.3 图片加载与渲染引擎选择图片加载这块是 OpenHarmony 上踩坑比较多的区域值得单独拿出来讲。网络图片来源基本都是服务端原图动不动就是单边 2000 像素的大图直接在Image.network里展示Android 上可能还能勉强撑住但在部分内存预算紧的开发板上很容易直接 OOM。我最后用的是cached_network_image插件配合CacheWidth参数让图片解码时直接把宽度压到设备需要的尺寸内存占用能少差不多一半。另一个渲染层面的选择是引擎。当时 OpenHarmony 适配版 Flutter 使用的还是 Skia 渲染引擎而 Flutter 官方主线从 3.10 开始逐步转向 Impeller主要是为了解决 Skia 在动画、模糊场景下的顿挫问题。我开发的页面里有一个封面图淡入淡出的过渡动画在低端设备上明显能感觉到 Skia 渲染帧率不稳后来我把动画改成简单的透明度插值并降低了首帧复杂度体感就舒服多了。这里给个结论别指望适配版立刻跟进 Impeller优化自己的图片尺寸和动画复杂度才是当前最可控的手段。提到图片顺便说一句CachedNetworkImage的磁盘缓存目录在 OpenHarmony 上的路径跟 Android 不一样但插件封装好了开发时不需要关心。唯一要注意的是清理缓存功能要自己写别直接拿 Android 平台的path_provider路径去删会产生一致性隐患。4.4 字体缩放与无障碍的小细节做菜场景里的用户画像中年人比例比一般 App 高得多视力条件和手指灵活度都各不相同所以字体缩放和无障碍我从一开始就当核心需求做不是最后补的。Flutter 的MediaQuery.textScaler在 OpenHarmony 适配版里是正常生效的所以只要代码里不用固定fontSize硬编码系统字体一放大页面上所有文本都会跟着变大。但问题往往出在组件尺寸上文本变大了按钮没有跟着变大或者两个步骤文本之间重叠了。我的解决方法是把关键点击区域的高度用ConstraintBox里的最小尺寸去约束同时在文字缩放超过 1.3 倍时切换为更紧凑的列表布局。无障碍语义这块我给步骤序号、收藏按钮、计时器都加了Semantics标签这样屏幕阅读器能读出“第二步牛腩冷水下锅焯水预计五分钟”这种完整信息。之前有次自测我用系统 TalkBack 朗读详情页发现在连续朗读步骤时标点符号会干扰断句后来在文案里统一加了句号分隔。这些细节点很小但对真实用户的帮助非常大。5. 组件通信与收藏状态同步实战5.1 组件通信的四种姿势对比“Flutter 组件通信怎么选”是群里被问烂的问题我这次项目里正好四种方式都用上了直接拿菜谱场景说人话。最朴素的姿势是构造函数传参父组件把数据直接塞给子组件适合静态展示型组件比如StepItem(step: step)简单直接没有任何学习成本。第二种是回调函数子组件需要把事件抛给父组件时用比如步骤卡片点击事件通过onStepTap回调交给页面容器处理适合单向事件流。第三种是InheritedWidget/Provider适合数据要跨多层或跨页面共享的场景典型例子就是收藏状态。第四种是事件总线适合完全解耦的跨模块通信比如用户在详情页完成了收藏操作同时要通知首页的推荐位刷新“猜你喜欢”的权重这种情况下用EventBus发一个FavoriteChangedEvent首页监听后主动拉新数据。我在项目里做了对比通信方式适用场景缺点构造参数传值父传子、静态数据多层传递繁琐回调函数子传父、单向事件嵌套过深易混乱Provider跨层共享、跨页同步状态粒度要设计好事件总线完全解耦模块通信全局事件难排查我的原则是能用几十行代码解决的通信就不上全局总线别为了架构而架构。详情页这种规模Provider 就覆盖了绝大部分场景。5.2 收藏功能跨页面状态同步的实现收藏功能是这个 App 里最能体现状态同步价值的地方。详情页要点亮收藏按钮收藏列表页要管理收藏条目首页卡片要判断是否显示收藏角标这三个地方读的是同一份收藏状态实际体验必须同步不能详情页收藏了首页角的图标还在那儿灰着。我的实现是全局维护一个FavoritesProvider挂在MultiProvider顶层。这个 Provider 里维护一个SetString _favoriteIds并提供toggleFavorite(String id)、isFavorite(String id)两个方法。详情页的收藏按钮只调用context.readFavoritesProvider().toggleFavorite(recipeId)然后ConsumerFavoritesProvider会自动重建按钮图标。收藏列表页因为用context.watchFavoritesProvider()监听收藏集合增删的瞬间列表就会自动刷新。整个过程完全不需要手动发事件通知。这里要提到一个我用过但后来抛弃的方案收藏状态不全局化而是放在详情页 Provider 里收藏发生后再用事件总线通知收藏列表页刷新。问题是启动收藏列表页的时候它要先查本地缓存才能知道自己收藏过什么再加上网络往返响应速度肉眼可见地慢。后来改成全局 Set 走了内存缓存收藏列表页打开即时有数据体验直接提升一个档位。如果你正在写类似功能收藏这种轻量但高频读写的状态直接全局化就好别绕弯子。经验跨页同步要先判断数据源头在哪。菜谱内容是服务端数据用 id 拉最新没问题收藏状态是用户本地行为全局内存缓存反而是最合理的谁读都一致不需要请求服务端。5.3 计时器与弹出层的小技巧菜谱详情页里“开始烹饪计时”这个交互我一开始以为就是放个Timer.periodic计数结果在 OpenHarmony 真机上遇到了麻烦。App 切后台锁屏Timer继续走了但 UI 不会刷新回到前台时秒数拉了一大截更严重的是一不小心在页面销毁时没有cancel定时器Dart 虚拟机直接打出 unhandled exception日志开头那串e/flutter (31173): [error:flutter/runtime/dart_vm_initializer.cc(41)]我第一次看到还以为是引擎崩了后来一查才知道是 Dart 侧未捕获异常的统一出口。计时器方案我后来收敛成两步Timer.periodic只负责前台 UI 刷新的秒数跳动真正的计时基准始终用DateTime.now()做差值计算。这样回前台时重新计算差值秒数不会跳变也免去了后台计时不准的问题。页面销毁时在dispose里无条件cancel定时器并且给计时回调加一个mounted判断双保险杜绝回调访问已销毁状态的问题。弹出层这里也提一个细节步骤弹出详情页的底部弹出层我用了showModalBottomSheet在 OpenHarmony 上如果弹出层里有图片弹层打开时背景页面可能出现白屏闪烁。这是适配版里比较常见的问题我的解决办法是在弹层打开前先预加载弹层里的图片到内存缓存这样弹出时不再触发首次解码。实测闪烁基本消失你可以直接套用。6. 真机调试中的常见问题与性能优化6.1 常见崩溃与异常排查这节把我在详情页从开发到真机自测过程中实际遇到并解决的核心异常清单整理出来。排在第一的就是前面说的dart_vm_initializer.cc(41)未捕获异常这个错误本身不是 bug而是 bug 的出口关键是看它后面跟的完整堆栈。例如有一次详情页加载失败我直接跳转了一个不存在的路由名称Navigator 抛了异常但页面没有 catch日志就落在了这个错误里。解决办法是给路由跳转统一封装一层guard捕获异常并做 toast 提示不让它冒泡到引擎层。第二个高频问题跟图片解码相关CachedNetworkImage加载一张超大尺寸图在低内存开发板上偶发崩溃或黑屏。崩溃的直接表现是 Flutter 的 Skia 引擎报OOM或者干脆无响应。我的处理是在列表和详情页统一使用ResizeImage约束解码尺寸同时给详情页的封面单独设置cacheWidth等于把图片解码前的尺寸直接压到需要的宽度内存压力顿时小了很多。这招 Android 端同样适用属于通用经验。第三个坑比较隐蔽在 OpenHarmony 模拟器里执行flutter logs拿日志会偶发丢行特别是在频繁滚动页面时报错时日志中间会断。排查方法是用 DevEco Studio 连接真机抓完整 hilog然后用hilog | grep flutter过滤 Flutter 引擎的输出。换了工具后我几次定位疑难 bug 都顺利多了。6.2 列表滚动性能优化实录详情页主要是单页滚动但步骤多时整个滚动链路的性能还是要抠一抠的。最直接的痛点是步骤图片在滚动进入视野时触发解码导致掉帧卡顿。我给步骤图加了cached_network_image的预缓存逻辑进入详情页后立刻把步骤列表里所有图片的 URL 提交给CachedNetworkImageProvider进行后台预取滚动时图片基本已经在磁盘缓存里了解码开销小很多。第二个重点是避免不必要的重建。详情页里有大量页内状态比如步骤完成标记、评分、收藏按钮如果整个详情页是被一个大Consumer包住的任何状态变化都会导致整页CustomScrollView重建滑动时就容易有迟滞感。我把页面拆成多个小范围Consumer每个Consumer只监听它负责的 Provider key其他区域完全不感知变化。实测滑动流畅度明显改善滚动帧率稳定多了。这个优化思路对你自己的 Flutter 项目同样有效记住一句话状态对象越大监听范围就要越小。第三个容易被忽略的点是RepaintBoundary。我把封面图、食材卡片、步骤时间轴这三个相对独立的绘制区域分别包上RepaintBoundary这样这些区域单独重绘时不会拖累整页渲染。虽然 RepaintBoundary 本身有开销但在滚动场景里收益大于成本。6.3 多设备适配的经验OpenHarmony 设备的碎片化比 Android 还夸张不同厂商的开发板、手机、平板屏幕规格差异很大。详情页适配我遇到的最典型表现是在一个 6.7 英寸的测试机上布局正常换到某款竖屏比例比较奇怪的开发板上封面图拉伸变形步骤文本间距也乱了。原因是封面图高度我固定成了MediaQuery.of(context).size.height / 2.8这个比例在普通屏上没问题但在超长屏上导致图片高度过大文案区域被挤下去。后来我改用AspectRatio将封面图锁定在 3:2 的宽高比再配合BoxFit.cover裁切不管屏幕多长多短封面比例始终稳定。食材卡片和步骤卡片的宽度也统一改为相对屏幕宽度的百分比约束而不是像素值硬编码。字体的多设备适配前面说了是跟随系统缩放这里我再补充一个点平板上做菜时用户往往会把设备立在支架上看页面底部如果放操作按钮容易被设备底部手势条遮挡。我给详情页底部操作栏加了SafeArea和viewPadding的合并处理保证按钮始终在安全区域内。这个坑在模拟器上基本不会出现但真机横屏、手势导航场景一开就露馅。多设备适配还有一个值得注意的问题覆盖图动画。步骤完成时序号圆点会有个缩放动画在低端设备上动画会显得发钝。我把动画时长从固定的 250ms 改成MediaQuery.disableAnimations为 true 时直接跳到终态既保住了动画体验又照顾了低性能设备和无障碍用户。最后聊点我这次的亲身感受整个详情页从搭环境到真机稳定跑通我大概花了一周多的时间其中近一半都消耗在环境配置和图片相关问题上。这也是我写这篇复盘的主要原因——Flutter for OpenHarmony 的资料远不如 Android 丰富很多坑都得靠自己在真机上撞。如果你的项目也在做类似的详情页我建议你把“图片尺寸压缩、State 监听范围控制、页面销毁时清理定时器”这三件事放在最早优先级处理这三点做好了后面基本能少踩一半的坑。另外有条件的话一定要尽早借一台真实的 OpenHarmony 设备来测试模拟器能验证逻辑但画布渲染、内存表现、手势交互只有真机不会骗你。
返回列表