
我一直觉得购物清单这种应用看着简单做好真的不容易。家里有两个人一起生活之后买东西变成了谁下班顺路带回来点什么于是我想自己写一个跨平台购物清单应用。技术路线几乎没有犹豫就选了Flutter一份Dart代码能跑多个平台再加上我正好在评估鸿蒙开发的适配情况拿它练手再合适不过。于是就有了这次Flutter框架跨平台鸿蒙开发实践一个名叫购物清单的应用用来高效管理日常购物计划。这篇文章会从选型、环境搭建、数据层、UI层到鸿蒙平台能力接入完整复盘一遍也会把踩过的坑一个个列出来适合手上有点Flutter基础、正打算往鸿蒙方向探路的读者参考。1. 为什么是 Flutter 鸿蒙而不是 ArkTS 从头写1.1 购物清单恰好是跨平台应用的试金石决定做一个项目之前我习惯先把需求收敛清楚。购物清单这东西表面看就是记几个字、勾几个框但真要当产品做核心需求至少有这些记录要买什么、给物品分类、到了超市能快速勾选已完成项、清理已购内容、偶尔分享给家人。这一套走下来增删改查 状态管理 本地持久化 平台能力调用一样都不缺。换句话说它麻雀虽小五脏俱全而且五脏的位置刚好覆盖了跨平台开发最常遇到的那些问题。正因为它功能边界清晰做起来不会像大型项目那样被各种需求裹挟可以用来专注验证一件事Flutter到底能不能在鸿蒙上给出一个体面的完整体验。反过来说如果购物清单这种轻量应用在鸿蒙上都做得磕磕绊绊那我对Flutter上鸿蒙的信心也会大打折扣。还有一个很私人的原因这个应用我是真的要在生活中用的不是做完演示就丢的Demo。因为自己会用每次改动都能拿到真实反馈这种正向循环让我在后来踩坑的时候始终有耐心往下挖。那为什么不用ArkTS从头写鸿蒙原生开发本身很成熟ArkTS ArkUI的声明式写法也确实高效。但问题在于我已经有Flutter和Dart的项目积累从一个成熟技术栈切到另一个成熟技术栈第一波成本全花在学习新语法、新组件体系、新状态管理方案上而不是花在解决购物清单这个具体问题上。对个人开发者来说这不是技术好坏的问题是投入产出比的问题。更何况我手头还有一些Android、iOS的小工具要维护购物清单用Flutter写未来同一套代码顺手覆盖其他平台的成本几乎为零。1.2 Flutter 渲染机制天然契合鸿蒙的底层结构有一点我得先说清楚Flutter之所以能在鸿蒙上做跨平台底层逻辑和React Native这类桥接方案完全不同。Flutter的UI渲染不依赖平台的原生控件。它的Widget树经过布局、绘制最终由自带的Skia或Impeller引擎直接画到一块GPU纹理上平台只需要提供一块可绘制的Surface和一套输入事件通道就够了。也就是说Flutter的UI是自己画出来的跟鸿蒙的ArkUI原生组件没有绑定关系。这对鸿蒙适配来说极其关键。鸿蒙系统自身的ArkUI是一套独立的声明式自绘体系组件从底层开始就和Android、iOS不一样。如果是桥接式方案你得把每个原生组件都在鸿蒙上重新适配一遍工作量是巨大的。但Flutter过去之后相当于你只是在一个新宿主平台上住进去平台提供一个空房间Flutter自带装修队把家具全部铺好不需要房东按租客的图纸打造每一件家具。理解了这一点你就知道为什么社区里Flutter适配鸿蒙的速度比很多别的跨平台框架要快。因为Flutter和宿主的耦合面非常薄要对接的只有生命周期、手势输入、渲染Surface、平台通道这几样基础能力。1.3 什么情况下不建议 Flutter 做鸿蒙讲完优点也必须讲边界不然就是误导。如果你要做的是重度依赖系统能力的应用——比如需要深度调用鸿蒙的分布式文件管理、多设备协同流转这类系统级能力那ArkTS ArkUI一定是更优先的选择。平台通道在技术上是通的但每个系统API的更新通道两侧的适配都有时间差你可能总要等生态去追。对启动速度和包体极度敏感的应用也要慎重。Flutter自绘引擎会额外引入几十MB动态库第一帧渲染也需要一定时间。购物清单不是这类应用所以我可以选Flutter但你不能拿我的选型套用到所有场景。还有团队因素如果团队完全没有Dart/Flutter经验而鸿蒙又是主力发布平台那直接学ArkTS可能比绕道跨平台更实际。Flutter的好处集中在UI密集型、逻辑层需要多端复用的项目上购物清单恰好命中全部条件这也是我拿它当第一个鸿蒙Flutter项目的原因。2. 环境搭建与工程创建从零到 Hello World2.1 版本匹配是鸿蒙环境的头号坑鸿蒙的Flutter支持不是Flutter官方主动提供的主要是开源社区在维护适配分支。这带来一个很实际的麻烦不同时期引入Flutter的方式差别很大网上教程经常互相矛盾。我按照老教程的流程flutter create创建完工程然后手工配置鸿蒙工程结构启动构建时直接来了个下马威The current configured Flutter SDK is not known to be fully supported。意思是当前Flutter SDK版本不在鸿蒙侧插件的白名单里。这种报错不是不能跑而是IDE或Gradle插件检测到版本组合有风险宁可先拦下来也不放你进一个未知的组合里。这里必须反复强调一个概念版本匹配是三条链Flutter SDK版本、鸿蒙SDK版本、DevEco Studio版本三者必须对齐。链上错一环后面全部白搭。我的建议是这样操作先安装DevEco Studio确认它能创建并运行一个纯ArkTS的Hello World工程先把原生侧环境打通。再安装Flutter SDK建议选stable分支不要追求最新beta。配置好环境变量让flutter命令在终端可用flutter doctor确认Dart和Flutter本身没问题。最后才去碰Flutter模块接入鸿蒙工程的事顺序不要反过来。很多人一上来就急着把Flutter塞进鸿蒙工程结果环境变量没配、SDK链不对报错后完全分不清是哪一环出的问题。2.2 工程创建鸿蒙壳 Flutter 模块的混合结构我最终采用的工程方案是原生工程为主、Flutter作为模块嵌入的混合结构。也就是说鸿蒙侧的EntryAbility负责应用启动和生命周期管理Flutter页面作为一个独立的容器页面挂载进去。粗看工程结构是这样的entry/鸿蒙应用入口负责启动Ability、加载Flutter容器flutter_module/Flutter模块目录包含lib/main.dart以及所有Dart业务代码oh-package.json5、build-profile.json5鸿蒙侧的依赖与构建配置这里要提醒一句如果你用的是支持OpenHarmony的Flutter SDK生成的项目目录结构又不太一样。不用死记结构核心是理解职责边界——Flutter侧只和Dart代码打交道鸿蒙侧只负责提供宿主容器两边的代码尽量别交叉。创建完成后第一件值得做的事是把Flutter模块里的默认计数器Demo跑起来确认Dart代码能在鸿蒙设备上渲染出画面。只有这一步通了后续所有业务代码的调试才有基础。2.3 真机调试hdc 工具链与热重载鸿蒙的调试工具链和Android很像它用的是hdcHarmonyOS Device Connector对应Android的adb。常用命令就那么几条先记这两个就够hdc list targets # 列出已连接的设备 hdc shell # 进入设备的shell环境我第一次连接真机时碰到的问题是设备列表里看不到手机。排查了半天原因很朴素开发者模式里的USB调试开关没打开或者hdc server没启动。需要注意的是hdc server和adb server是两套独立的工具不要混用我见过有人同时开着两个工具导致设备被占用、连接互相干扰。真机跑通之后一定要验证热重载是否生效。Flutter开发体验的底线就是改完代码能秒级看到变化。鸿蒙接入初期我遇到过热重载偶发不生效的情况表现为代码改了但界面没变化重启Flutter进程才恢复。如果你也遇到这个先别怀疑代码多半是模块连接状态的问题。3. 购物清单的数据层设计状态管理与本地持久化3.1 数据模型先想清楚再动手写代码之前我先把购物项的数据结构定了下来。购物清单里的一个条目至少应该包含这些信息class ShoppingItem { final String id; // 唯一标识用UUID final String name; // 商品名称必填 final int quantity; // 数量最小为1 final String unit; // 单位个/斤/瓶/包 final String category; // 分类生鲜/日用品/零食/其他 final bool checked; // 是否已购买 final String note; // 备注比如品牌偏好 final DateTime createdAt; // 创建时间 ShoppingItem({ required this.id, required this.name, this.quantity 1, this.unit 个, this.category 其他, this.checked false, this.note , required this.createdAt, }); }id字段我用字符串UUID而不是数据库自增数字这是刻意为之。UUID由Dart侧生成不依赖数据库将来如果要做多端同步、或者从Web端导入导出数据这个id可以直接复用不会出现冲突。category字段我选择了直接存字符串而没有单独建分类表。购物清单的分类本质上是轻量的用户自定标签不是强结构数据。用户改个分类名如果建了外键表反而要处理一堆关联更新不值当。这个取舍等到数据量大了、分类要起层级结构的时候再重新设计都不迟。3.2 状态管理Cubit 是购物清单最省心的选择Flutter的状态管理方案非常多我简单对比一下就知道该选哪个了方案特点适不适合购物清单Provider轻量依赖注入和UI耦合紧异步逻辑一多就散Bloc完整事件驱动状态机功能强大杀鸡用牛刀代码量大CubitBloc的简化版方法驱动状态很合适直观够用Riverpod编译期安全概念新要引入一组新概念学习成本高我最后选Cubit原因是购物清单的状态流转非常线性加载列表、添加一条、勾选一条、删除一条。整个过程没有什么并发事件流、没有多端同步竞争Cubit的async方法刚好和用户操作一一对应心智负担最小。设计上ShoppingListCubit对外暴露这样几个方法class ShoppingListCubit extends CubitShoppingListState { ShoppingListCubit(this._repository) : super(ShoppingListLoading()); Futurevoid load() async { ... } Futurevoid addItem(ShoppingItem item) async { ... } Futurevoid toggleCheck(String id) async { ... } Futurevoid deleteItem(ShoppingItem item) async { ... } Futurevoid clearChecked() async { ... } }状态类用不可变的List存储购物项同时用loading和error两个标志位来描述页面状态。这里有一个容易被忽略的点购物清单的临时状态也要纳入设计比如正在加载、保存失败。很多人只存数据不存状态导致界面在数据还没回来时白屏或者写入失败后用户完全无感知这些都是体验上的坑。3.3 本地数据库SQLite 与 DAO 层的意义购物清单的数据量虽然不大但我还是选了SQLite而不是用SharedPreferences或本地json文件。原因是购物场景里最常见的操作是单独更新某一条的勾选状态。用json方案每次更新都需要把整个文件反序列化、改字段、再序列化写回数据少还好条目一多就显得笨重。而SQLite天然支持按条件更新单条记录代价很小。建表语句我放在了初始化脚本里CREATE TABLE shopping_items ( id TEXT PRIMARY KEY, name TEXT NOT NULL, quantity INTEGER NOT NULL DEFAULT 1, unit TEXT NOT NULL DEFAULT 个, category TEXT NOT NULL DEFAULT 其他, checked INTEGER NOT NULL DEFAULT 0, note TEXT NOT NULL DEFAULT , created_at INTEGER NOT NULL ); CREATE INDEX idx_shopping_items_category ON shopping_items(category);DAO层的设计同样重要。我建了一个ShoppingItemDao把insert、updateChecked、queryByCategory、delete这些数据库操作全部集中在一个文件里。表面看这只是工程整洁问题实际不是。数据操作是整个应用里最容易被异步问题搞垮的地方把所有SQL集中起来一旦出现存储相关bug排查范围被锁死在一个文件里。后面就算要换数据库、换ORM改动范围也是可控的。3.4 Dart 异步模型Future.then 与微任务队列写购物清单的数据层时有一个问题困扰过我很久Future的then回调到底什么时候执行是立刻还是等所有代码跑完后来我确认了答案Dart是单线程事件循环模型Future完成后的回调会被放入微任务队列必须在当前同步执行栈结束后由事件循环优先取出执行。await本质上就是then的语法糖只不过让代码看起来像同步顺序。这个知识点不是纸上谈兵。购物清单里最常见的场景是用户勾选一个条目UI立即更新同时数据库写入。如果数据还没写完用户又取消勾选时序就要小心。我的做法是乐观更新先改内存状态、刷新UI再异步落库一旦失败再回滚。购物清单本身是低风险数据乐观更新带来的体验提升远大于出错的风险。再说直白一点在Flutter里操作数据库、读剪贴板、走平台通道全部是异步的。如果你对Future的调度时机没有准确认识就会出现数据明明写进去了列表刷新却拿到旧数据的诡异问题。别怀疑是数据库坏了多数是自己对异步时序的预期错了。3.5 数据层跨平台的红利把整个数据层写完我才真正感受到跨平台这件事的红利在哪里。ShoppingItem、DAO、Cubit这三层代码没有一行是鸿蒙特有的也没有一行是Android或iOS特有的。将来如果我想把这个购物清单搬到桌面端、甚至做成Flutter Web版本这些代码可以原封不动带走。跨平台最值钱的部分不是一套UI到处显示而是核心业务逻辑和数据结构与平台解耦。购物清单这个项目让这个道理变得非常具体。4. 界面实现清单页、添加编辑页与交互细节4.1 首页结构Tab 分类 列表展示购物清单的首页我用了很经典的AppBar TabBar TabBarView结构。Tab分成全部、生鲜、日用品、零食、其他全部Tab用同一份数据源做过滤展示每次切Tab就是对列表做一次Category条件筛选。列表项布局是这样的左侧是Checkbox中间是商品名称加上备注右侧显示数量和单位。已勾选的条目名称加删除线透明度降低一眼就能看出哪些已经买了。ListView.builder我加了fixed itemExtent强制设定固定行高。说实话不设itemExtent也能跑但设了之后滚动性能稳定很多因为ListView不用在滑动时反复调用item计算高度。购物列表动辄几十个条目这种微优化在低端设备上体感差异还是明显的。空状态也做了处理当没有购物项时页面中央显示一个空空的购物篮占位提示而不是一片空白。别小看这个细节用户第一次打开应用时如果看到白屏大概率会直接卸载。4.2 添加与编辑弹窗模式更快也更贴近真实场景添加购物项我用的是showModalBottomSheet弹窗而不是跳转独立页面。原因是补充一条购物项这个动作往往发生在收银台前、菜市场里属于急切场景弹窗比页面跳转快一个量级。弹窗里的表单字段有名称、数量、单位、分类、备注。名称必填数量最小为1。分类用一排FilterChip横向滚动展示每次只能选中一个。校验用Form GlobalKey 这样每个输入框不用单独维护校验状态。编辑和添加复用同一个弹窗组件传入一个初始item就是编辑模式不传就是添加模式。Dart的optional参数在这里特别顺手一个组件两种用法代码量省了不少。4.3 Navigator 与状态保持切换页面到底会不会丢状态Flutter Navigator切换页面后会丢失状态吗是新手非常高频的问题我在这个项目里也认真踩过一次。先给结论Navigator.push进入新页面时原页面并不会被销毁它的State还在路由栈里保留着。所以单纯的页面压栈状态不会丢。真正丢状态的是TabBarView这种场景。默认情况下TabBarView在两个Tab之间切换时非活跃页会被销毁切回来再重建。购物清单里就出了这么个事我在全部Tab里滚到了第30个条目切到生鲜再切回来列表直接回到了顶部。解决方式有三个组合起来用效果最好让页面State在Tab切换时保活用AutomaticKeepAliveClientMixin。class _ShoppingListPageState extends StateShoppingListPage with AutomaticKeepAliveClientMixinShoppingListPage { override bool get wantKeepAlive true; }给ListView一个PageStorageKey把滚动位置存下来。数据层共用同一个Cubit实例保证页面重建后数据能从内存里秒恢复而不是重新查数据库。还有一个容易混淆的点如果你在Cubit里做了状态重置比如每次页面重建都触发reload那即使Widget树没丢数据也会刷新看起来也像状态丢失。所以要让状态的生命周期尽量和页面生命周期解耦数据别依赖页面重建来加载。4.4 交互细节勾选动画、滑动删除与撤销购物清单的交互体验很大程度藏在细节里。勾选动作我做了乐观更新Checkbox变化的瞬间UI先变数据库写入异步进行失败再回滚。给用户的感受就是零延迟。左滑删除用Dismissible组件实现。这个组件有个坑如果列表项没有稳定的key参数滑动删除时Flutter会不知道哪条数据变了出现动画错乱。我给每条购物项都用ShoppingItem.id作为key这个问题就消失了。删除后的撤销功能是个加分项SnackBar弹出已删除商品并带一个撤销按钮。点击撤销时把之前删除的那条数据重新插入数据库。这要求DAO层的delete方法返回被删除的数据对象所以我设计接口时特意让delete返回ShoppingItem而不是void。清空已完成功能放在AppBar右上角点击后弹出AlertDialog二次确认防止误触。清空完成如果列表为空自动切换到空状态提示。关于TabBar的点击动画我保留了短动画但给TabBarView设置成不可横向滑动只允许点击切换。这样避免用户在快速左右滑动时看到半屏残影体验干净很多。5. 鸿蒙平台能力接入EventChannel 与 PlatformView5.1 为什么购物清单也绕不开平台能力纯Flutter能力能覆盖购物清单90%的功能但剩下10%不接原生就做不地道。我的需求只列了两个最刚需的把系统剪贴板里的文本比如牛奶 2瓶读进来快速生成购物项。把整个购物清单以文本形式分享给家人走系统分享能力。这两个需求都要碰系统服务在Flutter里就是MethodChannel和EventChannel的活。鸿蒙不是Flutter官方原生支持平台通道这部分必须自己接没有任何现成的插件给我抄。5.2 MethodChannel 与 EventChannel 的原理与区别MethodChannel是一次性问答Dart侧invokeMethod发起请求原生侧处理完返回一个值适合读取剪贴板这种请求-响应场景。EventChannel是持续直播Dart侧注册一个监听原生侧在事件发生时主动推送数据适合监听剪贴板变化这种场景。Dart侧的接入代码大致是这样的const _channel MethodChannel(com.example.shopping/clipboard); FutureString? readClipboard() async { return await _channel.invokeMethodString(readClipboard); } const _events EventChannel(com.example.shopping/clipboard_listener); void initClipboardListener() { _events.receiveBroadcastStream().listen((event) { // event 就是剪贴板里的文本 }); }鸿蒙侧的写法因为SDK版本不同差异较大这里不贴完整代码只讲关键思路在鸿蒙的Ability里获取系统剪贴板服务MethodChannel注册一个invokeMethod处理器读取缓存文本返回EventChannel则往Dart侧send数据。要特别注意在Ability的生命周期里注册和销毁通道监听防止内存泄漏。通道接入时我踩过最低级的错误channel name两边不一致。Dart侧写的是com.example.shopping/clipboard鸿蒙侧写成了com.example.shopping/Clipboard大小写不同。这种错误不会报错只是调用静默失败或返回空值非常难排查。通道名前缀统一用小写复制粘贴的时候要格外小心。另外原生侧完成任务后回调Dart侧时要回到主线程再传数据。Flutter框架约定channel回调最终在主线程执行原生如果在线程池里直接回调可能出现线程时序问题。这个细节鸿蒙适配的稳定版本应该处理了但接的时候最好检查一下。5.3 我为什么最终砍掉了 PlatformView 功能设计初稿里有个功能点击购物项可以查看某类商品在商品页里的详细介绍。要内嵌网页就得在Flutter里嵌入一个原生WebView走PlatformView机制。PlatformView在鸿蒙上试验的结果不算差但问题也足够明显手势冲突最突出。原生Web页面里的滚动会被Flutter侧的手势体系抢断经常划两下就触发边缘返回。渲染时序不一致。PlatformView和Flutter自绘UI出现上一帧滞后快速滚动列表时原生Web内容像粘在残影上。生命周期管理复杂。WebView在页面切换、应用退后台时要手动同步鸿蒙Ability的生命周期稍有不慎就白屏。最后我做了取舍砍掉内嵌WebView改为点击购物项后调用系统分享菜单里的打开链接能力用外部浏览器看详情。这不是PlatformView不可用而是对购物清单这种轻量工具来说为一个不常用的功能承担手势冲突和生命周期维护成本不划算。这个决策过程让我对Flutter跨平台有了更清醒的认识跨平台的原则不是什么都要在Flutter里面解决而是用最少的原生依赖完成功能。如果你真的要大量内嵌Web内容还是得认真评估鸿蒙上的webview插件维护状态再做决定。6. 踩坑现场从编译到打包的完整排查记录6.1 SDK 白名单报错The current configured Flutter SDK is not known to be fully supported创建鸿蒙Flutter混合工程时构建插件直接给了一条警告当前配置的Flutter SDK不是已知受支持的版本。这个报错频繁出现在Flutter官方刚发新版、鸿蒙适配插件还没跟上的窗口期SDK检测到版本不在白名单里宁可先禁用部分功能也不放你进组合。我的排查链路是这样的先确认当前Flutter版本flutter --version记录版本号。去鸿蒙侧工程的配置里查Flutter adapter插件对SDK版本的要求。检查flutter通道确认当前是stable还是beta。切回stable版本重新flutter pub get再flutter clean构建。最后我是把Flutter SDK从beta切回stable才解决的。这个经验不是让你追新版本恰恰相反鸿蒙适配是走在Flutter官方后面的追beta版本只会让自己更难受。老老实实用stable能少一半莫名其妙的问题。6.2 Gradle 插件命令式 apply 报错构建过程中出现了一个很典型的Gradle报错you are applying flutters main gradle plugin imperatively using the apply method。新版本Flutter的Gradle插件要求用plugins DSL声明而不是老式的直接在build.gradle里apply。排查过程不复杂但需要耐心根据报错信息定位到对应的build.gradle文件。把apply plugin那一行迁移到plugins块里。确认插件版本和Flutter SDK版本匹配。我特别想提醒一点这种Gradle升级类报错不要凭记忆改文件。先搜索一下当前Flutter版本对应的迁移说明很多时候它只是一行注释提示不修正也能构建成功但风险不小。我在这个项目里选择响应提示做了迁移日志里的警告消失后续构建稳定性明显提升。6.3 模拟器上 EventChannel 收不到数据真机却正常有一次我改完剪贴板监听功能在鸿蒙模拟器上怎么复制文本Dart侧都收不到事件。当时第一反应是代码写错了但拿着同一套代码跑到真机上事件一切正常。排查顺序是这样的先在模拟器上用MethodChannel读取剪贴板发现能正常返回说明通道本身没坏。怀疑EventChannel接入有细节问题但真机复现不了。最后发现是模拟器的系统剪贴板服务事件通知机制不完整复制文本只更新了内容并没有触发全局回调。这个教训很实用凡是涉及系统服务、硬件能力的通道功能一定要以真机为基准。模拟器可以用来排查UI和纯逻辑但不能用来验证系统能力。如果你在模拟器上遇到类似通道静默失效先换真机试一下别急着改代码。6.4 打包优化包体、启动与渲染引擎的取舍Flutter新版引入的Impeller渲染引擎用GPU预编译shader替代了Skia运行时的指令生成目标是消除首帧卡顿。我实测购物清单在鸿蒙设备上跑起来确实更顺滑动画掉帧少了。但我也看到社区里有人反馈某些GPU驱动在Impeller下出现闪烁。如果你的设备上出现这样的情况可以临时用flutter run --no-enable-impeller切回Skia做对比再决定是否彻底禁用。包体优化做了三件事按CPU架构分包不打包所有架构产物。清理依赖。购物清单早期加过几个演示性质的包release构建前全部移除包体降了不少。图片资源压缩并转成WebP格式。我不建议在功能还没定型前就折腾包体优化那是浪费时间。等UI和交互都稳定了再回头做这些收益更大。6.5 数据库文件的沙箱路径变化最后补充一个比较隐蔽的坑。sqflite初始化时数据库文件一般放在应用私有目录。鸿蒙上APP的沙箱目录在不同版本和不同权限模式下路径会有差异。症状是应用升级覆盖安装后购物清单突然变成空的。排查时我在DAO初始化里加了一行日志打印数据库文件的绝对路径和exists状态发现升级后路径没变但目录被系统重建了旧数据库文件没有跟着迁移。解决思路有两个方向一是初始化时检测数据库文件不存在但备份文件存在自动恢复二是把购物清单的关键数据定期导出为JSON放到应用文档目录升级后检测到数据库为空且JSON存在时执行恢复导入。我采用了后者因为JSON导出还能顺便满足分享购物清单的需求一份代码两处用。跨平台开发时存储位置会随平台环境变化这个意识值得提前建立。不要假设某个目录是永远可靠的初始化时多做一次存在性检查能省掉很多用户反馈数据丢了的售后。做完这个购物清单项目我最大的体会是Flutter跨平台开发鸿蒙这条路真实可走但前提是别把跨平台理解成零成本。它省掉的是重复学习UI框架和业务逻辑迁移的时间省不掉的是平台差异的适配和耐心排查的过程。如果一件事能让你少走弯路那一定是你自己走过弯路之后总结出来的。最后分享一个小技巧我在项目里把所有通道调用、平台相关能力都收口到了一个PlatformAdapter接口后面Dart业务层只依赖这个接口。将来不管是鸿蒙、Android还是其他平台换一个平台的实现类就行。这个习惯我从购物清单开始养成了建议你也试试。