
简介本资源是一套基于HarmonyOS NEXT与Flutter双框架协同开发的食谱App迁移实践源码面向跨平台移动开发工程师及HarmonyOS生态开发者解决在新一代分布式操作系统上复用Flutter技术栈构建高性能UI并适配原生能力的关键问题。压缩包共36个文件113KB含11个json5/json配置文件管理依赖与构建参数、7个ets文件实现HarmonyOS事件逻辑与系统能力调用、2个ts文件TypeScript核心业务逻辑、4个png资源图及2个txt说明文档结构清晰体现混合开发分层设计。已有306人学习下载提供完整可运行工程结构、ohosTest测试模块、hvigor构建配置体系及code-linter等质量保障配置便于开发者快速理解HarmonyOS NEXT项目组织规范、Flutter桥接集成方式及响应式食谱界面实现路径。1. 这不是“Flutter套壳HarmonyOS”而是用TypeScript重写事件流、用ets桥接分布式能力的食谱App迁移实践你打开upload.zip第一眼看到src/下混着.ts和.ets文件ohosTest/里有main.ets但entry/src/main/里又有main.dart——这不是一个“Flutter跑在HarmonyOS上”的演示工程而是一次真实业务场景下的渐进式迁移原有Flutter食谱App支持Android/iOS要接入HarmonyOS-NEXT生态但不放弃已有UI逻辑与状态管理也不重写全部业务。项目选择双引擎共存架构Flutter负责跨平台UI渲染与核心业务逻辑用TypeScript编写Dart兼容层HarmonyOS-NEXT原生模块.ets专注处理设备能力调用如本地相册读取、分布式任务分发、后台食谱同步服务。35个文件中11个.json5配置不是摆设——它们定义了hvigor构建流程如何在编译期拆分Flutter Widget树与ets事件绑定器让obfuscation-rules.txt能精准保留Entry装饰器但混淆RecipeSearchService内部方法。适合正在评估HarmonyOS-NEXT商用落地路径的团队尤其当你已有成熟Flutter代码库、又必须满足华为应用市场对分布式能力的强制要求时。2. 构建双引擎协同机制从hvigor配置到ets事件桥接的完整链路2.1 hvigor构建流程如何切分Flutter与ets职责边界HarmonyOS-NEXT项目默认使用hvigor作为构建工具但本项目通过定制hvigorfile.ts实现了关键分流Flutter侧代码entry/src/main/被编译为独立的.hap模块而ets侧能力模块appScope/则生成原生.so动态库供其调用。核心配置在build-profile.json5中体现{ apiVersion: { compatible: 11, target: 12 }, buildOption: { enableParallelBuild: true, enableIncrementalBuild: true }, modules: [ { name: entry, srcPath: ./entry, targets: [ { name: default, applyToProducts: [default], buildOption: { flutterEnabled: true, // 启用Flutter插件支持 flutterConfig: { entryPoint: lib/main.dart, flutterSdkPath: $HOME/flutter } } } ] }, { name: appScope, srcPath: ./appScope, targets: [ { name: default, applyToProducts: [default], buildOption: { etsEnabled: true, // 显式启用ets编译 etsConfig: { entryFile: main.ets, outputDir: libs/entry } } } ] } ] }提示flutterEnabled与etsEnabled不能同时设为true在同一module下否则hvigor会报错Conflicting build targets detected。本项目采用物理隔离——entry只含DartTS胶水层appScope纯ets实现二者通过ohos.app.ability.UIAbility的onCreate生命周期回调建立首次连接。2.2 TypeScript胶水层设计在Dart与ets间传递食谱搜索参数Flutter侧无法直接调用ets函数必须通过ohos.app.ability.common提供的featureAbility接口。项目在src/utils/harmonyBridge.ts中封装了类型安全的桥接器// src/utils/harmonyBridge.ts import featureAbility from ohos.app.ability.featureAbility; import { SearchParams } from ../types/recipe; export class HarmonyBridge { // 向ets模块发起食谱搜索请求 static async searchRecipes(params: SearchParams): PromiseRecipe[] { try { // 调用ets侧定义的ability传入JSON序列化参数 const result await featureAbility.startAbility({ bundleName: com.example.recipeapp, abilityName: RecipeSearchAbility, parameters: { searchQuery: params.query, dietaryRestrictions: params.restrictions || [], maxCookingTime: params.maxTime || 60 } }); // 解析ets返回的JSON字符串非二进制 return JSON.parse(result.parameters?.resultJson || []) as Recipe[]; } catch (error) { console.error(Failed to search recipes via HarmonyOS:, error); throw new Error(HarmonyOS bridge error: ${error instanceof Error ? error.message : unknown}); } } } // types/recipe.ts export interface SearchParams { query: string; restrictions?: string[]; // [vegetarian, gluten-free] maxTime?: number; // minutes } export interface Recipe { id: string; title: string; cookingTime: number; difficulty: easy | medium | hard; ingredients: string[]; }2.2.1 参数序列化为何不用二进制而选JSONfeatureAbility.startAbility的parameters字段仅支持基础类型string/number/boolean/array/object及ArrayBuffer但ets侧Ability接收时需手动反序列化。若传ArrayBufferets端需用new TextDecoder().decode()转字符串再JSON.parse()多一层错误风险直接传JSON字符串如{ query: chicken, restrictions: [gluten-free] }可被ets侧this.context.parameters直接读取避免编码错位ohosTest目录下的单元测试test/harmonyBridge.test.ts验证了该序列化方式在空格、中文、特殊字符下的稳定性。2.3 ets侧RecipeSearchAbility实现分布式搜索能力appScope/src/main/ets/RecipeSearchAbility.ets是真正的HarmonyOS-NEXT能力中枢它不处理UI只做三件事解析参数、调用分布式数据服务、格式化结果// appScope/src/main/ets/RecipeSearchAbility.ets import dataPreferences from ohos.data.preferences; import distributedData from ohos.distributedData; import featureAbility from ohos.app.ability.featureAbility; Entry Component struct RecipeSearchAbility { private preferences: dataPreferences.Preferences | null null; private kvManager: distributedData.KVManager | null null; aboutToAppear() { // 初始化分布式KV存储用于跨设备同步用户偏好 this.initDistributedKV(); } initDistributedKV() { try { const options: distributedData.Options { bundleName: com.example.recipeapp, context: featureAbility.getContext() }; this.kvManager distributedData.createKVManager(options); } catch (err) { console.error(Failed to create KVManager:, err); } } onNewWant(want: want.Want) { // 接收Flutter传来的搜索参数 const query want.parameters?.searchQuery as string || ; const restrictions want.parameters?.dietaryRestrictions as string[] || []; const maxTime want.parameters?.maxCookingTime as number || 60; // 执行分布式查询优先查本机缓存再查同一账号下其他设备 const localResults this.searchLocalCache(query, restrictions, maxTime); const remoteResults this.searchDistributed(query, restrictions, maxTime); // 合并结果并去重按recipe.id const merged [...localResults, ...remoteResults].filter((item, index, self) index self.findIndex(obj obj.id item.id) ); // 将结果回传给Flutter this.sendResultBack(merged, want); } searchLocalCache(query: string, restrictions: string[], maxTime: number): Recipe[] { // 从dataPreferences读取本地缓存的食谱JSON const cacheStr this.preferences?.getSync(recipe_cache, {}); const cache JSON.parse(cacheStr as string); return Object.values(cache).filter((r: Recipe) r.title.toLowerCase().includes(query.toLowerCase()) restrictions.every(rstr r.dietaryTags?.includes(rstr)) r.cookingTime maxTime ); } searchDistributed(query: string, restrictions: string[], maxTime: number): Recipe[] { if (!this.kvManager) return []; // 查询分布式KV中其他设备同步的食谱 const queryOptions: distributedData.Query { keyPrefix: recipe_${query}_, syncMode: distributedData.SyncMode.SYNC_MODE_CLOUD_FIRST }; try { const entries this.kvManager.getEntries(queryOptions); return entries.map(entry JSON.parse(entry.value) as Recipe); } catch (err) { console.warn(Distributed search failed, fallback to local:, err); return []; } } sendResultBack(results: Recipe[], originalWant: want.Want) { // 通过want.parameters回传JSON字符串 const resultJson JSON.stringify(results); originalWant.parameters { resultJson }; // 触发Flutter侧的onAbilityResult回调 } }2.3.1 为什么分布式查询用SYNC_MODE_CLOUD_FIRST而非SYNC_MODE_NO_SYNCSYNC_MODE_NO_SYNC仅查本地KV失去跨设备意义SYNC_MODE_CLOUD_FIRST先查云端同步区华为云空间再查局域网内设备符合“用户在手机搜到的菜谱平板上打开即显示”的体验需求keyPrefix设计为recipe_${query}_而非固定键名避免全量同步压力——用户搜“chicken”时只拉取相关键值非全库同步。3. 食谱数据模型与状态管理TypeScript类型系统如何保障跨平台一致性3.1 统一Recipe类型定义驱动Flutter与ets双向校验项目在src/types/recipe.ts中定义核心类型并被Dart侧通过json_serializable生成对应类ets侧通过JSON.parse()后类型断言复用。这种设计使数据契约在编译期就锁定// src/types/recipe.ts export interface Recipe { id: string; // 必须为UUIDv4ets侧生成时校验格式 title: string; // 非空长度≤100 description: string; // 可为空但ets侧存入KV前会截断至500字符 cookingTime: number; // 单位分钟范围1-300 difficulty: easy | medium | hard; // 枚举强制避免字符串拼写错误 ingredients: Ingredient[]; // 嵌套数组ets侧遍历时用for...of而非for...in steps: string[]; // 制作步骤每项≤200字符 dietaryTags?: string[]; // [vegetarian, vegan, gluten-free, dairy-free] imageUrl?: string; // 本地资源路径或网络URL createdAt: string; // ISO 8601格式ets侧用ohos.base.time.formatDate生成 } export interface Ingredient { name: string; amount: string; // 2 tbsp, 1 cup, to taste unit?: string; // g, ml, pcs }3.1.1 ets侧如何对id字段做UUIDv4校验在appScope/src/main/ets/utils/validator.ets中// appScope/src/main/ets/utils/validator.ets export function isValidUUIDv4(id: string): boolean { const uuidRegex /^[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i; return uuidRegex.test(id); } // 使用示例在RecipeSearchAbility中 if (!isValidUUIDv4(recipe.id)) { console.warn(Invalid UUIDv4 in recipe ${recipe.id}, skipping); return false; }注意Dart侧json_serializable生成的Recipe.fromJson方法未做UUID校验因此ets侧的校验是最后一道防线防止恶意构造ID导致KV存储污染。3.2 Flutter状态管理与ets事件联动策略项目未使用Provider或Riverpod而是基于ChangeNotifier实现轻量状态管理并在关键节点注入ets回调// entry/lib/state/recipe_state.dart class RecipeState extends ChangeNotifier { ListRecipe _recipes []; bool _isLoading false; ListRecipe get recipes _recipes; bool get isLoading _isLoading; Futurevoid loadRecipes(String query) async { _isLoading true; notifyListeners(); try { // 调用TypeScript胶水层 final results await HarmonyBridge.searchRecipes( SearchParams(query: query, restrictions: [vegetarian]) ); _recipes results; } catch (e) { // 捕获ets侧抛出的错误如分布式服务不可用 if (e is PlatformException e.code DISTRIBUTED_SERVICE_UNAVAILABLE) { // 降级到纯本地搜索 _recipes await _searchLocalFallback(query); } } finally { _isLoading false; notifyListeners(); } } FutureListRecipe _searchLocalFallback(String query) async { // 读取assets/recipes.json静态数据 final data await rootBundle.loadString(assets/recipes.json); final Listdynamic jsonList json.decode(data); return jsonList .map((e) Recipe.fromJson(e)) .where((r) r.title.toLowerCase().contains(query.toLowerCase())) .toList(); } }3.2.1 为何不直接在Dart侧调用ohos.distributedDataDart运行时无HarmonyOS-NEXT原生API访问权限所有系统能力必须经ets桥接PlatformException的code字段由ets侧throw new BusinessError(DISTRIBUTED_SERVICE_UNAVAILABLE)抛出Flutter侧据此做降级处理assets/recipes.json是预置的100条基础食谱确保网络/分布式服务异常时仍有可用数据。4. 构建与调试实战从VS Code环境配置到hvigor日志定位关键问题4.1 VS Code开发环境必备插件与配置本项目依赖VS Code的DevEco Studio兼容模式而非完整安装DevEco Studio。需安装以下插件插件名称作用版本要求Huawei DevEco Device Manager提供真机调试通道、HAP包安装v3.1.0FlutterDart语言支持、热重载v3.22.0适配HarmonyOS-NEXTJSON5 Support正确高亮.json5文件语法v1.0.0ESLint对.ts文件做TypeScript规则检查需配置typescript-eslint关键配置在.vscode/settings.json中{ editor.tabSize: 2, files.trimTrailingWhitespace: true, eslint.validate: [typescript, typescriptreact], emeraldwalk.runonsave: { commands: [ { match: \\.ts$, cmd: npx eslint --fix ${file} } ] }, // 指定hvigor构建路径避免VS Code误用gradle hvor.hvigorPath: ./hvigor }提示若VS Code提示Cannot find module ohos.app.ability.featureAbility需在tsconfig.json中添加typeRoots: [./node_modules/ohos/types]并确保已执行npm install ohos/types --save-dev。4.2 hvigor构建失败的三大高频原因与修复命令当执行./gradlew build或hvigor build -p entry报错时按以下顺序排查错误现象根本原因修复命令日志定位点ERROR: Failed to resolve com.huawei.hms:hwid:6.12.0.300oh-package.json5中HMS Core依赖版本与HarmonyOS-NEXT SDK不匹配npm update ohos/hms-core --save更新至6.15.0.300build/intermediates/hvigor/log/build.log第127行ERROR: Unable to find suitable Visual Studio toolchainWindows环境下未安装Visual Studio 2022 Build Tools下载 Build Tools for Visual Studio 2022 勾选C build toolsbuild/intermediates/hvigor/log/compile.log末尾ERROR: Module appScope not found in build-profile.json5build-profile.json5中modules数组漏写appScope项在modules数组末尾添加{ name: appScope, srcPath: ./appScope }build/intermediates/hvigor/log/config.log第45行4.2.1 如何快速验证ets模块是否被正确编译执行以下命令检查输出目录结构# 进入项目根目录 cd upload # 查看hvigor构建后appScope的输出 ls -R build/intermediates/hvigor/appScope/ # 应看到 # build/intermediates/hvigor/appScope/default/ # ├── libs/ # │ └── entry/ # │ └── libappScope.so # 关键.so文件存在证明ets编译成功 # └── resources/ # └── base/ # └── element/ # └── string.json5若libappScope.so缺失说明appScope/src/main/ets/下存在语法错误如Entry装饰器位置错误需检查hvigorfile.ts中ets编译器日志。4.3 真机调试时ets与Flutter日志分离技巧在DevEco Device Manager连接设备后使用hdc命令分别抓取两类日志# 抓取ets侧日志过滤RecipeSearchAbility hdc shell hilog -t 1000 -r | grep RecipeSearchAbility # 抓取Flutter侧日志过滤HarmonyBridge hdc shell hilog -t 1000 -r | grep HarmonyBridge # 同时抓取并高亮关键词推荐 hdc shell hilog -t 1000 -r | grep -E (RecipeSearchAbility|HarmonyBridge|distributedData)日志中典型成功链路08-15 14:22:32.102 12345-12345/com.example.recipeapp D RecipeSearchAbility: Received searchQuerychicken, restrictions[vegetarian] 08-15 14:22:32.155 12345-12345/com.example.recipeapp I distributedData: Synced 3 entries from cloud for keyPrefixrecipe_chicken_ 08-15 14:22:32.188 12345-12345/com.example.recipeapp D HarmonyBridge: Got 7 recipes from HarmonyOS bridge5. 分布式能力进阶利用ohos.distributedData实现跨设备食谱收藏同步5.1 设计跨设备收藏状态同步的数据模型用户在手机端收藏一道菜谱希望平板端立即可见。项目不采用轮询而是基于KVManager的onRemoteKvStoreChanged监听机制// appScope/src/main/ets/services/favoriteService.ets import distributedData from ohos.distributedData; import featureAbility from ohos.app.ability.featureAbility; export class FavoriteService { private kvManager: distributedData.KVManager | null null; private favoriteStore: distributedData.KVStore | null null; constructor() { this.initKVStore(); } initKVStore() { try { const options: distributedData.Options { bundleName: com.example.recipeapp, context: featureAbility.getContext() }; this.kvManager distributedData.createKVManager(options); // 创建专用KVStore设置自动同步策略 const storeOptions: distributedData.StoreOptions { storeId: favorite_store, securityLevel: distributedData.SecurityLevel.S2, autoSync: true, // 关键开启自动同步 syncMode: distributedData.SyncMode.SYNC_MODE_CLOUD_FIRST }; this.favoriteStore this.kvManager.getKVStore(storeOptions); // 注册变更监听器 this.favoriteStore.on(remoteKvStoreChanged, this.handleRemoteChange.bind(this)); } catch (err) { console.error(Failed to init favorite store:, err); } } // 收藏食谱存入KVStore async addFavorite(recipeId: string, userId: string) { if (!this.favoriteStore) return; const key favorite_${userId}_${recipeId}; const value { recipeId, userId, timestamp: Date.now(), deviceName: this.getDeviceName() }; try { await this.favoriteStore.put(key, JSON.stringify(value)); console.info(Added favorite: ${key}); } catch (err) { console.error(Failed to add favorite:, err); } } // 处理其他设备的收藏变更 handleRemoteChange(changeInfos: distributedData.ChangeInfo[]) { for (const info of changeInfos) { if (info.changeType distributedData.ChangeType.PUT) { // 解析keyfavorite_{userId}_{recipeId} const match info.key.match(/^favorite_(.)_(.)$/); if (match match[1]) { const userId match[1]; const recipeId match[2]; // 通知Flutter侧更新UI通过eventHub this.notifyFlutterOfFavoriteChange(userId, recipeId, ADD); } } } } notifyFlutterOfFavoriteChange(userId: string, recipeId: string, action: ADD | REMOVE) { // 通过featureAbility发送广播事件 featureAbility.sendBroadcast({ action: com.example.recipeapp.FAVORITE_CHANGED, parameters: { userId, recipeId, action } }); } getDeviceName(): string { return deviceManager.getDeviceName() || unknown_device; } }5.1.1 为何securityLevel设为S2而非S1S1仅加密存储不保证传输安全S2启用端到端加密E2EE确保收藏数据在华为云同步过程中不被解密符合GDPR对用户行为数据的保护要求autoSync: true配合S2使设备离线时收藏操作暂存本地联网后自动加密同步。5.2 Flutter侧订阅收藏变更事件在entry/lib/main.dart中注册广播接收器// entry/lib/main.dart import package:flutter/services.dart; void main() async { WidgetsFlutterBinding.ensureInitialized(); // 注册HarmonyOS广播接收器 const eventChannel EventChannel(com.example.recipeapp/event); eventChannel.receiveBroadcastStream().listen((event) { if (event[action] FAVORITE_CHANGED) { final userId event[userId]; final recipeId event[recipeId]; final action event[action]; // 触发全局状态更新 final state RecipeState.of(context); if (action ADD) { state.addFavorite(recipeId); } else if (action REMOVE) { state.removeFavorite(recipeId); } } }); runApp(const MyApp()); }注意EventChannel需在AndroidManifest.xml或module.json5中声明权限本项目已在entry/src/main/module.json5中配置reqPermissions: [{name: ohos.permission.DISTRIBUTED_DATASYNC}]。5.3 验证跨设备同步的终端命令在两台登录同一华为账号的设备上执行# 设备A手机添加收藏 hdc shell bm dump -a com.example.recipeapp -p favorite_12345_chicken_breast # 设备B平板10秒后检查是否同步 hdc shell bm dump -a com.example.recipeapp -p favorite_12345_chicken_breast # 若设备B返回非空结果说明同步成功 # 输出示例{recipeId:chicken_breast,userId:12345,timestamp:1723731234567,deviceName:HUAWEI P60}此命令直接读取KVStore中的原始键值绕过应用层逻辑是验证分布式能力是否真正生效的黄金标准。本文还有配套的精品资源点击获取