☰
Flutter跨平台鸿蒙开发实践:购物清单应用从0到1完整复盘
2026/9/30 8:03:07 网站建设 项目流程

我一直觉得购物清单这种应用看着简单,做好真的不容易。家里有两个人一起生活之后,买东西变成了"谁下班顺路带回来点什么",于是我想自己写一个跨平台购物清单应用。技术路线几乎没有犹豫就选了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 World

2.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版本,三者必须对齐。链上错一环,后面全部白搭。

我的建议是这样操作:

  1. 先安装DevEco Studio,确认它能创建并运行一个纯ArkTS的Hello World工程,先把原生侧环境打通。
  2. 再安装Flutter SDK,建议选stable分支,不要追求最新beta。
  3. 配置好环境变量,让flutter命令在终端可用,flutter doctor确认Dart和Flutter本身没问题。
  4. 最后才去碰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很像,它用的是hdc(HarmonyOS 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 Cubit<ShoppingListState> { ShoppingListCubit(this._repository) : super(ShoppingListLoading()); Future<void> load() async { ... } Future<void> addItem(ShoppingItem item) async { ... } Future<void> toggleCheck(String id) async { ... } Future<void> deleteItem(ShoppingItem item) async { ... } Future<void> 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个条目,切到"生鲜"再切回来,列表直接回到了顶部。

解决方式有三个,组合起来用效果最好:

  1. 让页面State在Tab切换时保活,用AutomaticKeepAliveClientMixin。
class _ShoppingListPageState extends State<ShoppingListPage> with AutomaticKeepAliveClientMixin<ShoppingListPage> { @override bool get wantKeepAlive => true; }
  1. 给ListView一个PageStorageKey,把滚动位置存下来。

  2. 数据层共用同一个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 与 PlatformView

5.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'); Future<String?> readClipboard() async { return await _channel.invokeMethod<String>('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检测到版本不在白名单里,宁可先禁用部分功能也不放你进组合。

我的排查链路是这样的:

  1. 先确认当前Flutter版本,flutter --version记录版本号。
  2. 去鸿蒙侧工程的配置里查Flutter adapter插件对SDK版本的要求。
  3. 检查flutter通道,确认当前是stable还是beta。
  4. 切回stable版本,重新flutter pub get,再flutter clean构建。

最后我是把Flutter SDK从beta切回stable才解决的。这个经验不是让你追新版本,恰恰相反,鸿蒙适配是走在Flutter官方后面的,追beta版本只会让自己更难受。老老实实用stable,能少一半莫名其妙的问题。

6.2 Gradle 插件命令式 apply 报错

构建过程中出现了一个很典型的Gradle报错:you are applying flutter's main gradle plugin imperatively using the apply method。新版本Flutter的Gradle插件要求用plugins DSL声明,而不是老式的直接在build.gradle里apply。

排查过程不复杂但需要耐心:

  1. 根据报错信息定位到对应的build.gradle文件。
  2. 把apply plugin那一行迁移到plugins块里。
  3. 确认插件版本和Flutter SDK版本匹配。

我特别想提醒一点:这种Gradle升级类报错,不要凭记忆改文件。先搜索一下当前Flutter版本对应的迁移说明,很多时候它只是一行注释提示,不修正也能构建成功,但风险不小。我在这个项目里选择响应提示做了迁移,日志里的警告消失,后续构建稳定性明显提升。

6.3 模拟器上 EventChannel 收不到数据,真机却正常

有一次我改完剪贴板监听功能,在鸿蒙模拟器上怎么复制文本,Dart侧都收不到事件。当时第一反应是代码写错了,但拿着同一套代码跑到真机上,事件一切正常。

排查顺序是这样的:

  1. 先在模拟器上用MethodChannel读取剪贴板,发现能正常返回,说明通道本身没坏。
  2. 怀疑EventChannel接入有细节问题,但真机复现不了。
  3. 最后发现是模拟器的系统剪贴板服务事件通知机制不完整,复制文本只更新了内容,并没有触发全局回调。

这个教训很实用:凡是涉及系统服务、硬件能力的通道功能,一定要以真机为基准。模拟器可以用来排查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还是其他平台,换一个平台的实现类就行。这个习惯我从购物清单开始养成了,建议你也试试。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询