news 2026/10/8 2:51:02

Flutter在OpenHarmony上实战:开发美食烹饪助手“今日推荐”功能

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
Flutter在OpenHarmony上实战:开发美食烹饪助手“今日推荐”功能

说实话,第一次在OpenHarmony设备上把Flutter跑起来的时候,心情还挺复杂的。折腾了两个晚上才把环境配通,中间一度怀疑是不是自己打开方式不对。但当我自己写的“美食烹饪助手”首页卡片真的在鸿蒙平板上渲染出来,滑动跟手、动画流畅的时候,那种踏实感,确实是看一晚上文档都体会不到的。

这篇内容就是记录我用 Flutter for OpenHarmony 从零开发一个美食烹饪助手的完整过程,重点写大家最关心的“今日推荐”功能是怎么从 0 到 1 落地的。包含环境搭建、状态管理、推荐逻辑、本地缓存、真机调试和各类坑点。适合两类人看:一类是已经在 Flutter 上有基础、想试试 OpenHarmony 生态的同学;另一类是刚听说 Flutter 能跑在鸿蒙上、想评估这套方案靠不靠谱的人。相信我,看完你会少走很多弯路。

1. 项目背景与整体设计思路

1.1 为什么选 Flutter 而不是 ArkTS 原生

先说结论:工具型、内容型、UI 密集型的 App,在 OpenHarmony 上用 Flutter 是划算的;如果是要深度调用系统底层能力、强依赖分布式软总线特性的应用,现阶段老老实实用 ArkTS 或者 ArkUI 更省心。

美食烹饪助手属于典型的内容工具型 App:页面结构固定(首页推荐、菜谱详情、我的收藏)、卡片式 UI 密集、需要频繁刷新和动画过渡。这类 App 的开发瓶颈从来不在系统 API 的调用深度,而在 UI 迭代效率。Flutter 一套代码六个平台跑——Android、iOS、Web、Windows、macOS、Linux,现在再加上 OpenHarmony,对团队来说意味着鸿蒙生态的适配成本被压缩到一个量级以内。

而且说实话,OpenHarmony 的北向语言是 ArkTS,语法上接近 TypeScript,对于纯前端背景的开发者确实友好。但 Flutter 的 Dart 语言 + 自带渲染引擎这套组合,在 UI 一致性和动画表现力上有天然优势。你在 Flutter 里写好了的组件、状态管理、路由方案,到了 OpenHarmony 上几乎不用改,这种“技能复用”的价值,做过跨端迁移的人都懂。

1.2 “今日推荐”功能的需求拆解与方案选型

“今日推荐”听起来简单,拆开看其实有四个核心诉求:

一是每日更新。用户每天早上打开 App,看到的不应该是昨天那一批菜,否则就是“假推荐”。推荐结果必须跟日期绑定,每天自动轮换。

二是有推荐理由。光给一个菜名和图片不够,用户需要一个“为什么今天推荐这道菜”的理由,比如“时令食材”“热量适中”“适合工作日快手菜”。这是美食类 App 区别于普通菜谱列表的关键体验。

三是加载要快。App 冷启动后,推荐区如果转菊花转个三秒,用户大概率就退出了。首屏必须秒开,剩余内容异步补全。

四是离线可用。做饭的场景经常在厨房,网络信号未必好。推荐结果必须有本地缓存能力,没有网也能打开昨天的菜谱。

技术选型上,状态管理我选了 Provider。原因很直接:轻量、官方文档齐全、社区案例多,而且对 OpenHarmony 这种生态还不够成熟的平台来说,用越主流的方案,踩坑后能搜到的解决方案就越多。Riverpod 虽然设计更严谨,但引入的概念偏多,新手容易绕晕;Bloc 在复杂业务场景有优势,但对这个小功能属于大材小用。饮食类推荐的核心逻辑是规则引擎 + 加权打分,不是实时流式计算,Provider 的 ChangeNotifier 机制完全够用。

2. 环境准备与项目初始化

2.1 三步搞定 OpenHarmony 开发环境:DevEco Studio 与 Flutter SDK 配置

OpenHarmony 的开发工具链现在比两年前成熟多了,但第一次配置还是有门槛。我直接把验证过的步骤给出来。

第一步,安装 DevEco Studio 4.0 及以上版本。下载地址在 OpenHarmony 官网,选 Windows 或者 macOS 的对应包。安装完成后,重点检查一件事——SDK 路径。DevEco Studio 默认会装一套 OpenHarmony SDK,通常在你用户目录下的AppData\Local\OpenHarmony\Sdk(Windows)或者~/Library/OpenHarmony/Sdk(macOS)。这个路径一定要记清楚,后面配环境变量要用。

第二步,获取 Flutter for OpenHarmony 的 SDK。注意这里跟普通 Flutter 不是一回事,你需要克隆官方适配分支:

git clone -b ohos-4.0 https://gitee.com/openharmony/flutter_flutter.git

我用的版本是 Flutter 3.7.12 的 ohos 适配分支,对应 OpenHarmony API 10。如果你想用更新的版本,先去官方仓库看分支列表,确认跟手头 DevEco Studio 的 SDK 版本匹配,这个匹配关系比你想的重要得多。

第三步,配置环境变量。需要配置三个:

# 指向 OpenHarmony SDK 根目录 export DEVECO_SDK_HOME="$HOME/ohos-sdk" # PATH 加入 flutter 工具 export PATH="$PATH:$HOME/flutter_ohos/bin" # 开启 ohos 平台支持 export FLUTTER_OHOS=true

配置完跑flutter doctor,如果看到 OpenHarmony 那一栏是绿色的勾,说明环境通了。我当时卡了很久才发现是DEVECO_SDK_HOME指到了 DevEco Studio 的安装目录而不是 SDK 目录,环境变量这东西,错了不报错,只有到构建时才疯给你看。

2.2 创建工程并理解 OpenHarmony 平台在 Flutter 中的角色

环境通了之后,创建工程跟普通 Flutter 项目几乎一样:

flutter create --platforms ohos cooking_assistant

创建完成后你会看到项目里多了一个ohos/目录,这就是 OpenHarmony 的工程壳。它内部是标准 OpenHarmony 工程结构,包含entry模块、oh-package.json5等。整套东西看着陌生,但核心心法很简单:Flutter 是泥瓦匠,OpenHarmony 是房东。ohos/目录这个“房子”提供房间、水电(系统能力、窗口、事件投递),你的 Flutter 代码负责把房间装修漂亮。在 OpenHarmony 上跑 Flutter 应用,本质上是通过 Flutter 引擎的 OpenHarmony 适配层,把 Dart 代码渲染成图像输出到系统窗口。

此时我建议你在pubspec.yaml里先把依赖加好:

dependencies: flutter: sdk: flutter provider: ^6.1.1 shared_preferences: ^2.2.2 intl: ^0.18.1

intl是为了处理日期格式化和国际化,后面写推荐逻辑时能用到。shared_preferences做缓存,轻量够用。为什么不引入网络请求库?因为第一阶段我准备用本地 JSON 模拟数据源,先把功能跑通。等后续接真实接口时再加 dio 也不迟。

注意:pub 包版本要选择支持 ohos 平台的。绝大多数纯 Dart 包在 OpenHarmony 上没问题,但涉及原生插件(比如 shared_preferences)则要求插件实现 ohos 端。好在 OpenHarmony 社区已经把常用插件的适配做了大部分,shared_preferences、path_provider 这类高频包都有 ohos 实现,直接在 pub.dev 搜shared_preferences_ohos就能找到。如果某个包没有 ohos 实现,项目启动时会直接报 MissingPluginException。

3. “今日推荐”核心功能实现

3.1 数据模型与本地菜谱库设计

写任何功能的第一步,不是 UI 不是状态,而是数据模型。我把菜谱模型设计成下面这样:

class Recipe { final int id; final String name; final String description; final List<String> ingredients; final int duration; // 烹饪时长,单位分钟 final String difficulty; // 简单 / 中等 / 困难 final int calories; // 千卡 final String imagePath; // 本地图片资源路径 final List<String> tags; // 标签,如 ["时令蔬菜", "快手菜", "高蛋白"] final double rating; // 用户评分,用于加权 Recipe({ required this.id, required this.name, required this.description, required this.ingredients, required this.duration, required this.difficulty, required this.calories, required this.imagePath, required this.tags, required this.rating, }); factory Recipe.fromJson(Map<String, dynamic> json) { return Recipe( id: json['id'], name: json['name'], description: json['description'], ingredients: List<String>.from(json['ingredients']), duration: json['duration'], difficulty: json['difficulty'], calories: json['calories'], imagePath: json['imagePath'], tags: List<String>.from(json['tags']), rating: (json['rating'] as num).toDouble(), ); } }

我把菜谱数据放在assets/data/recipes.json里,管这个叫“本地菜谱库”。一开始放了 20 道菜:番茄炒蛋、红烧排骨、清蒸鲈鱼、宫保鸡丁、上汤娃娃菜……每道菜都把食材、热量、时长、标签写清楚。为什么用 JSON 而不是 Dart 对象直接初始化?好处是可扩展——以后接远程更新,服务端下发同样的 JSON 结构,前端解析逻辑不需要动一行代码。这就是前后端分离思维在客户端数据层的体现。

3.2 推荐引擎:日期种子 + 加权评分的规则实现

“今日推荐”最核心的逻辑就是推荐引擎。我没有上机器学习(大材小用),而是用了“日期种子随机 + 多因子加权评分”的规则方案。

核心思路:每天的推荐列表要稳定且不同。稳定指的是,同一天内无论用户刷新多少次,推荐结果都是一样的——不能每次刷新都变,否则用户会觉得系统有问题。不同指的是,换一天必须整体换掉。

用日期做随机种子天然满足这两个要求:

class RecommendationEngine { static const List<Recipe> _allRecipes = [...]; // 菜谱库 static List<Recipe> generate(DateTime date, {int count = 6}) { // 以日期为种子,保证每天的推荐序列稳定 final seed = date.year * 10000 + date.month * 100 + date.day; final random = Random(seed); final scored = _allRecipes.map((recipe) { var score = recipe.rating * 10 + random.nextDouble() * 10; // 季节加权:根据月份匹配时令食材标签 if (_isSeasonal(recipe, date.month)) { score += 15; } // 工作日优先推荐快手菜 if (_isWeekday(date) && recipe.duration <= 30) { score += 10; } // 周末推荐耗时菜 if (_isWeekend(date) && recipe.duration > 45) { score += 8; } return _ScoredRecipe(recipe, score); }).toList(); scored.sort((a, b) => b.score.compareTo(a.score)); return scored.take(count).map((e) => e.recipe).toList(); } static bool _isSeasonal(Recipe recipe, int month) { if (month >= 3 && month <= 5) return recipe.tags.contains('春季食材'); if (month >= 6 && month <= 8) return recipe.tags.contains('夏季开胃'); if (month >= 9 && month <= 11) return recipe.tags.contains('秋季润燥'); return recipe.tags.contains('冬季暖胃'); } }

这里有个细节值得说:为什么评分公式是评分 * 10 + 随机数 * 10?因为菜谱库里的 rating 范围在 3.5 到 5.0 之间,乘以 10 后是 35 到 50 分,随机项 0 到 10 分,季节加权 15 分。这个权重关系是经过设计的——季节加权和场景加权必须能影响最终排序,但不能完全淹没基础评分。如果随机项是 0 到 100,那每天推荐出来的菜就是纯随机,用户评分失去意义;如果只有基础评分,那每天都推荐同几道高分菜,失去“每日新鲜感”。

推荐引擎输出的是第一道菜的“主推菜”和后五道的“副推列表”。主推菜会展示大图和推荐理由,副推列表则是紧凑卡片。

3.3 Provider 状态管理与组件通信实战

推荐引擎写好了,接下来是怎么把数据推送到 UI。

先说组件通信。Flutter 的组件通信分几种:父子传参(构造参数传递)、子父回调(回调函数)、跨层级共享(InheritedWidget/Provider)。在今日推荐场景,问题是这样的:HomePage里嵌着TodayRecommendationSection,里面又嵌着RecommendationCard,而数据是异步加载的。如果每个层级都用构造函数传参,页面会变成一座传参屎山——改一个字段,整条链路上的构造函数全得改。

Provider 方案就是为解决这个问题存在的。在main.dart顶部注入:

void main() { runApp( ChangeNotifierProvider( create: (_) => RecommendationProvider()..loadToday(), child: const CookingApp(), ), ); }

RecommedationProvider 继承 ChangeNotifier:

class RecommendationProvider extends ChangeNotifier { List<Recipe> _todayRecipes = []; bool _loading = true; bool _loadedFromCache = false; List<Recipe> get todayRecipes => _todayRecipes; Recipe? get featured => _todayRecipes.isEmpty ? null : _todayRecipes.first; bool get loading => _loading; Future<void> loadToday() async { final now = DateTime.now(); // 先读缓存,让首屏秒开 final cached = await CacheHelper.read('recommend_${now.year}_${now.month}_${now.day}'); if (cached != null) { _todayRecipes = cached; _loadedFromCache = true; _loading = false; notifyListeners(); } // 再走引擎生成最新推荐(后台异步比较) final generated = RecommendationEngine.generate(now); if (!_loadedFromCache || generated.isNotEmpty) { _todayRecipes = generated; _loading = false; notifyListeners(); // 写缓存,下一次冷启动直接用 await CacheHelper.write('recommend_${now.year}_${now.month}_${now.day}', generated); } } Future<void> refresh() async { _loading = true; notifyListeners(); await Future.delayed(const Duration(milliseconds: 600)); _todayRecipes = RecommendationEngine.generate(DateTime.now()); _loading = false; notifyListeners(); } }

子组件读状态的姿势很关键。用context.watch<T>()会把组件注册为 Provider 的监听者,Provider 一旦notifyListeners(),组件自动重建。用context.read<T>()则是一次性读取,不监听。推荐列表卡片用 watch,按钮点击时读数据用 read——这一点搞反了会出现局部刷新乱套或者性能浪费。

实际写页面时,我是这样在一个组件里同时用到两种读取方式的:

class TodayRecommendationSection extends StatelessWidget { @override Widget build(BuildContext context) { final provider = context.watch<RecommendationProvider>(); if (provider.loading && provider.todayRecipes.isEmpty) { return const Center(child: CircularProgressIndicator()); } return Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ _FeaturedCard(recipe: provider.featured!), const SizedBox(height: 12), _RecommendationListView(recipes: provider.todayRecipes.skip(1).toList()), ], ); } }

刷新按钮的做法是:点击时不需要等 UI 跟随状态变化,直接触发操作即可。

IconButton( icon: const Icon(Icons.refresh), onPressed: () => context.read<RecommendationProvider>().refresh(), )

3.4 推荐卡片 UI 实现:从布局到动效细节

推荐卡片的 UI 我花了不少心思。主推卡用的是一个 16:9 的大图卡片,左上角贴“主厨推荐”的标签,底部是菜名、推荐理由和关键参数行。副推列表是一行横向滚动的紧凑卡片。

先看主推卡的代码:

class _FeaturedCard extends StatelessWidget { final Recipe recipe; const _FeaturedCard({required this.recipe}); @override Widget build(BuildContext context) { return Container( height: 260, decoration: BoxDecoration( borderRadius: BorderRadius.circular(20), image: DecorationImage( image: AssetImage(recipe.imagePath), fit: BoxFit.cover, ), ), child: Stack( children: [ // 底部渐变遮罩,保证文字可读性 Positioned.fill( child: DecoratedBox( decoration: BoxDecoration( borderRadius: BorderRadius.circular(20), gradient: LinearGradient( begin: Alignment.topCenter, end: Alignment.bottomCenter, colors: [ Colors.transparent, Colors.black.withOpacity(0.65), ], ), ), ), ), Positioned( left: 16, bottom: 16, child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( recipe.name, style: const TextStyle( fontSize: 24, fontWeight: FontWeight.bold, color: Colors.white, ), ), const SizedBox(height: 6), Text( _buildReason(recipe), style: const TextStyle(color: Colors.white70, fontSize: 14), ), const SizedBox(height: 8), Row( children: [ _InfoChip(icon: Icons.timer_outlined, text: '${recipe.duration} 分钟'), const SizedBox(width: 8), _InfoChip(icon: Icons.local_fire_department_outlined, text: '${recipe.calories} 千卡'), const SizedBox(width: 8), _InfoChip(icon: Icons.restaurant_outlined, text: recipe.difficulty), ], ), ], ), ), ], ), ); } }

卡片切换的动效用的是 Flutter 自带的方向性滑动:给推荐区包一个AnimatedSwitcher,每次推荐结果更新时,新卡片从右侧滑入。这个动效应验了 Flutter 的两大卖点之一:UI 表现力的跨平台一致性。在 OpenHarmony 上跑这个动画,跟 Android 上的表现几乎没有差别,因为动画计算和渲染都在 Flutter 引擎层完成,跟系统 UI 框架没有半毛钱关系。

4. 数据持久化与加载优化

4.1 本地缓存策略:让“今日推荐”离线也能用

美食烹饪助手的使用场景决定了它必须能在弱网甚至无网环境下工作——厨房信号差是客观现实。所以我给推荐结果做了本地缓存。

实现方式用的是 shared_preferences,key 的设计是关键:

Future<void> _writeTodayCache(List<Recipe> recipes) async { final prefs = await SharedPreferences.getInstance(); final dateKey = DateFormat('yyyyMMdd').format(DateTime.now()); prefs.setString('recommend_$dateKey', jsonEncode(recipes)); }

仔细看这个 key——我把日期拼进去了。好处是:每天的推荐天然是不同 key,缓存不会互相覆盖;坏处是:如果用户一个月不打开 App,本地会堆积三十个 key 的垃圾数据。所以顺手做了一层清理:读取时检查历史 key 里超过一周的,直接删掉。

static Future<void> _cleanupOldCache() async { final prefs = await SharedPreferences.getInstance(); final keys = prefs.getKeys().where((k) => k.startsWith('recommend_')); final today = DateTime.now(); for (final key in keys) { final datePart = key.split('_').last; final cachedDate = DateTime.parse(datePart); if (today.difference(cachedDate).inDays > 7) { prefs.remove(key); } } }

你可能要问:缓存的数据和当天生成的推荐数据冲突怎么办?我的策略是缓存优先。loadToday()先读缓存,如果命中就直接展示,不重新生成了。这样有两个好处:一是冷启动速度极快,读个字符串比跑推荐引擎快一个数量级;二是同一天的展示结果绝对一致,不会出现用户早上看完,中午刷新变成另一批菜的情况。只有缓存没有命中时才走引擎生成。

4.2 图片资源管理与对 App 包体积的克制

菜谱卡片要好看,图片是核心,但图片也是 OpenHarmony 应用包体积的大头。20 道菜的图片如果都用高清 JPG 塞进assets/,每张 500KB 算少的,一个长图轮播页面做下来保底 10MB。对于工具型 App 来说,这个体积在鸿蒙生态里算是个负担。

我的做法是分场景处理:主推大图的精度要求高,用 1080p 的 JPG;副推小卡片是 300px 见方的缩略图,用 WebP 格式,单张控制在 20KB 以内。另外在pubspec.yaml里对 assets 做了路径分包:

assets: - assets/data/recipes.json - assets/images/hero/ # 主推大图 - assets/images/thumb/ # 副推缩略图

这样资源能按需加载,目录结构也清晰。等以后接入后端接口,把图片 URL 化就不用管体积问题了——但离线优先的策略建议保留,缓存图片到本地也走shared_preferences存 URL 列表,配合文件系统缓存,那都是后话。

5. 常见问题与排查实录

5.1 Flutter 新建项目后跑不起来的三个高频原因

这个问题在热搜词里出现频率高到离谱,我猜很多人第一步就挂在环境上。我总结遇到的三个高频原因:

第一个是OpenHarmony SDK 版本和 Flutter 适配分支不匹配。DevEco Studio 4.0 对应 API 10,配 3.7.12 的 ohos 分支没问题;但如果你的 DevEco Studio 升级到 4.1(API 11),还拿老的 ohos 分支跑,构建产物会出现链接错误。解法:去 flutter_flutter 仓库的ohos分支列表里找你 SDK 版本对应的分支,别贪新。

第二个是Gradle 下载超时导致构建失败。OpenHarmony 工程构建也是走 Gradle 供应链,在国内环境下首次构建会有大量的下载依赖,网络稍差就挂。解法:在ohos/目录下的build-profile.json5里配置镜像源,或者用 DevEco Studio 内置的 SDK 管理器先把依赖预下载完再跑 flutter。

第三个是环境变量问题导致设备识别不了。症状是 flutter run 说找不到设备,但 DevEco Studio 明明能连上。检查FLUTTER_OHOS=true是否设置了,以及 hdc(OpenHarmony 的设备连接工具,类似 Android 的 adb)是否加入了 PATH。排查命令:

hdc list targets

无输出就说明 hdc 没找到设备,优先检查 USB 调试模式和驱动;如果输出乱码,检查 hdc 版本是否跟设备系统版本匹配。OpenHarmony 的 hdc 版本跟设备版本不匹配时会出现“能发现设备但无法连接”的诡异状态,这时候手动更新本机 hdc 到对应版本即可。

5.2 状态管理在 OpenHarmony 上的坑与对应解法

我以为 Provider 这套机制在 OpenHarmony 上是天然跑通的,毕竟它只是 Dart 层的代码逻辑,跟系统无关。但实际开发中踩了两个不大不小的坑。

第一个坑:状态更新了,UI 不刷新。排查了半天,最后发现是构造 Provider 时,create里用了异步方法但没有正确处理。在create: (_) => RecommendationProvider()..loadToday()这段代码里,loadToday()是异步的,而create执行完会立即调用 Provider 的首次读取。如果首帧 UI 在异步方法完成前就被 build,读到的就是空列表。我的处理方式是给 Provider 内部加了 loading 状态,UI 在_loading && _todayRecipes.isEmpty时显示加载圈,等异步通知后重建,问题就解决了。看起来简单,但这其实是个 Provider 新手十个人八个会遇到的细节。

第二个坑:某些子组件不在 Provider 作用域内,导致context.watch直接抛 ProviderNotFoundException。这个多数是因为根 widget 是MaterialApp而不是ChangeNotifierProvider直接包在页面外层。我的建议是 Provider 尽量放在runApp的最外层,包住整个 App 而非某个页面,这样任何一级组件都能安全读取。

第三个坑和 Impeller 有关。Flutter 3.7 在 Android 上默认开启 Impeller 渲染引擎,但 ohos 分支的 Impeller 适配还不完善,跑起来会有偶发性的渲染撕裂。我的解法是显式关闭 Impeller,切回 Skia:

flutter run --no-enable-impeller

或者在main.dart里写死:

void main() { FlutterRendering.impellerEnabled = false; runApp(...); }

等 ohos 分支后续版本把 Impeller 适配补齐了再打开不迟。

5.3 真机调试技巧和常见报错速查表

在 OpenHarmony 真机上调试,体验跟 Android 的 adb 逻辑类似但不完全一样。我常用的一套组合:

# 查看设备列表 hdc list targets # 安装产物到设备 hdc install entry/build/default/outputs/default/entry-default-signed.hap # 看设备日志(按关键字过滤) hdc shell hilog | grep -i flutter # 抓取崩溃栈 hdc shell hilog -b D | grep 'FATAL\|Exception'

hilog 是 OpenHarmony 的日志系统,平时 Flutter 的 debugPrint 输出也会走这里。如果你在设备上运行 App,刷日志主要靠 hilog,而不是 flutter logs。下面这张速查表是我这次开发过程中整理的高频问题清单,直接抄作业:

现象可能原因排查与解法
flutter run 找不到 ohos 设备FLUTTER_OHOS 未设置 / hdc 不在 PATH设置环境变量,确认hdc list targets有输出
构建时 GN 配置报错Flutter 适配分支与 SDK 版本不匹配检查 flutter --version,切换对应 ohos 分支
冷启动后推荐卡片空白Provider 异步加载时序问题给 Provider 加 loading 状态,UI 判断空列表时显示加载圈
真机跑起来图片加载失败assets 未正确声明检查 pubspec.yaml 的 assets 路径,目录不能有变量或通配符
推荐列表每次刷新都不一样随机种子不是日期确认用DateTime.now()转年月日后创建 Random,不能直接用 now 本身
状态刷新但 UI 不动context.watch / context.read 混用需要监听重建的地方用 watch,只在事件回调里触发读数据
页面切换卡顿图片分辨率过高,真机解码耗时副推卡片用 WebP 缩略图,主推大图控制在 1080p 以内
hilog 刷不到 flutter 输出hilog 缓存过大 / 过滤不严用-b D调大缓冲区,按 tag 过滤后再看

这里面最值得强调的就是“推荐列表每次刷新都不一样”的坑。我一开始在RecommendationEngine.generate里写的是Random(DateTime.now().millisecondsSinceEpoch),结果每次进入页面都会生成新序列,用户下拉一次换一批菜,测试反馈说“系统是疯了吧”。改成以年月日组成整数做种子后,同一天内任何时候进入,结果都稳定一致。这个细节,做推荐类功能的人早晚会遇到。

5.4 集成模式的补充:把 Flutter 模块塞进已有 OpenHarmony 工程

最后补充一个很多同学问过的问题:不是从零创建 Flutter 工程,而是把 Flutter 模块集成到已有 OpenHarmony 工程里。这种场景在 OpenHarmony 上目前还不像 Android 的 Flutter AAR 方案那么顺手,但路子是通的。

核心思路是:在已有 ohos 工程的ohos/模块里引用 Flutter 的 HAR 包,然后在 ArkTS 侧用 Flutter 提供的容器组件挂载 Flutter 页面。具体做法是找 flutter 构建产物里的flutter.har,塞进ohos/entry/libs/下,接着在entry/src/main/ets/pages/Index.ets里声明容器节点。步骤不复杂,但版本约束很敏感,Flutter HAR 版本和 OpenHarmony SDK 版本必须严格对应。

如果你是单模块 App 从零起步,建议直接走flutter create路线;如果团队已经有完整的 OpenHarmony 应用,只是想渐进接入某几个 Flutter 页面,再考虑集成模式。步子别迈太大,Flutter 模块集成到原生工程的调试链路比纯 Flutter 工程复杂不少,没有充分理由我不推荐一上来就这么干。

结尾

开发这个美食烹饪助手的“今日推荐”功能,前后花了约一星期。真要说最深的体会,不是 Flutter 在 OpenHarmony 上跑得有多顺畅,而是跨端开发的边界感:Flutter 负责 UI 和业务逻辑这一层,在鸿蒙生态里是真的能打;但涉及系统能力对接、平台适配这些地方,必须清楚哪些是成熟的、哪些还在快速迭代。

最后分享一个我自己的小习惯:在 OpenHarmony 上调试 Flutter 应用,每次改动代码后不要急着上真机,先flutter analyze跑一遍静态检查,再flutter build hap --debug确认构建产物能出。高频的小步验证看起来多花几分钟,实际是省时间最快的办法——总好过改了十行代码直接上真机,然后对着 hilog 里几百行日志大海捞针。

下一步我准备把这个项目的推荐逻辑从纯本地规则升级成接口拉取模式,接入真实的食材库和用户收藏数据做个性化排序,到时候再写一篇跟你们分享。

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

3D视觉引导机器人焊接实战笔记:从选型到调试完整指南

说个比较现实的场景&#xff1a;焊工老师傅越来越难招&#xff0c;年轻人不愿意干这行&#xff0c;车间里一台焊接工位往往要养两到三个人&#xff0c;还得忍受烟尘、弧光和参差不齐的焊接质量。过去几年很多工厂尝试上焊接机器人&#xff0c;结果发现真正卡脖子的不是机器人本…

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

CTF杂项隐写全解析:从图片LSB到音频摩斯码解压获取flag

1. 拿到附件之后&#xff0c;先做的几件事&#xff1a;文件结构与考点拆解1.1 解压附件&#xff0c;把家底清出来国赛杂项的隐写题&#xff0c;从来不是让你拿着工具对着一个文件硬试。拿到手的第一步永远是“清点情报”。这次这道steganography_challenge&#xff0c;附件解压…

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

JavaWeb图书馆系统MySQL环境适配实战指南

简介&#xff1a;这是一套面向Java Web初学者与高校课程设计者的图书馆管理系统完整源码&#xff0c;基于ServletJSPMySQL技术栈实现&#xff0c;适用于Java Web基础教学、课程实训及毕业设计参考。系统覆盖图书管理、读者借阅、用户权限控制等核心业务&#xff0c;完整呈现MVC…

作者头像 李华
网站建设 2026/10/8 2:48:22

TCP与UDP协议深度解析:从三次握手到抓包排障实战

1. 先搞清楚TCP和UDP到底在解决什么问题前阵子帮客户排查一个线上问题&#xff1a;服务端偶尔出现连接超时&#xff0c;客户端日志一片飘红的"connection timed out"。抓包看了半天&#xff0c;问题不是出在端口或防火墙&#xff0c;而是TCP握手阶段的重传退避太长&a…

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

零成本搭建本地AI助手:Ollama与提示词工程实战指南

做开发的这些年&#xff0c;我越来越明白一件事&#xff1a;真正的效率提升&#xff0c;不是多收藏一个AI工具清单&#xff0c;而是亲手搭一个属于自己的AI助手。今天这篇我不想整虚的&#xff0c;直接给你一套经过反复验证的方案——10分钟能跑起来&#xff0c;成本为0&#x…

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

SRAM原理与工程实践:从六晶体管单元到GPU缓存设计

1. 什么是静态随机存储器&#xff08;SRAM&#xff09;&#xff1f;它不是“快一点的U盘”&#xff0c;而是数字电路的呼吸中枢你可能在拆过旧手机主板时&#xff0c;见过芯片上印着“SRAM”几个小字&#xff1b;也可能在刷显卡BIOS失败后&#xff0c;维修师傅说“SRAM缓存校验…

作者头像 李华