在 Flutter 社区里问"鸿蒙该怎么适配",大部分回答都是让你转 ArkTS。但我的实际经验是:如果你的团队已经积累了 Flutter 代码,与其推翻重来,不如先看看 Flutter 在鸿蒙上的真实可行性。这篇文章就基于一个真实的宠物驱虫记录器项目,完整拆解我用 Flutter 跨平台框架跑通鸿蒙开发的全过程——从环境搭建到数据模型设计,从状态管理到组件通信,最后把应用装上鸿蒙真机。整个项目麻雀虽小,但五脏俱全,包含了列表、表单、日期计算、持久化、跨页面传参这些几乎所有 App 都会遇到的基础场景,很适合作为 Flutter 鸿蒙开发的入门练手项目,也适合正在评估"要不要用 Flutter 接鸿蒙需求"的团队做技术预研。
1. 先用 Flutter 做鸿蒙,还是直接梭哈 ArkTS
1.1 鸿蒙应用开发的现状:不是二选一,而是共存
先说结论:鸿蒙生态确实在快速扩张,纯 ArkTS 开发的鸿蒙原生应用数量也在涨,但这不代表 Flutter 没有位置。鸿蒙目前的开发路线很清晰——上层应用可以吃 OpenHarmony 的能力,底层是方舟编译器在跑 ArkTS。而 Flutter 这边,从 3.27 版本开始已经有一些社区和官方层面的适配成果,专门的鸿蒙适配分支 (flutter_flutter 的 ohos 分支) 已经能让 Flutter 应用跑在鸿蒙设备上。换句话说,Flutter 不是"不能做鸿蒙",而是"需要走正确的适配通道"。
这个项目我选择的路线是:用 Flutter 主工程写 UI 和业务逻辑,通过 ohos 分支提供的构建能力打出 HAP 包,最后在鸿蒙手机上安装运行。这套流程本质上和"Flutter 打 APK、打 IPA"是同一个套路,只是目标平台从 Android/iOS 换成了 OpenHarmony。对于一个已经跑过 Flutter Android 工程的团队来说,学习成本主要集中在环境配置和构建链路上,Dart 代码本身不需要做大规模改写。
1.2 宠物驱虫记录器为什么适合做跨平台样板
选这个项目不是随手抓的,它有几个非常契合 Flutter 鸿蒙适配验证的特质:
- 纯本地数据,不依赖第三方云服务,不需要考虑云厂商对鸿蒙的 SDK 适配情况
- 数据结构清晰,宠物和驱虫记录是典型的主从表关系,适合验证 Provider 的多模型管理能力
- 页面跳转、表单输入、列表渲染、日历日期计算,覆盖了移动端最常用的组件类型
- 可以离线运行,真机调试时不用纠结网络权限和 HTTPS 证书问题
1.3 Flutter 与 ArkTS 的取舍逻辑
| 维度 | Flutter + ohos 分支 | 纯 ArkTS |
|---|---|---|
| 团队学习成本 | 只需学鸿蒙构建流程 | 从语言到框架全量重学 |
| 代码复用率 | Android/iOS/鸿蒙三端共用 | 仅鸿蒙端 |
| 生态成熟度 | 仍处适配期,部分原生插件不可用 | 官方全力推进 |
| 性能表现 | 自绘引擎,UI 层性能可控 | 方舟运行时,原生优先 |
我个人的判断是:如果是鸿蒙独占的轻量工具类应用,ArkTS 确实更顺。但如果你的目标是"一套代码覆盖三个平台",Flutter 的价值在于业务层的绝对复用率——你只需要处理鸿蒙平台上的特殊适配,而不需要把页面粒度重写一遍。
2. 搭建 Flutter 鸿蒙开发环境,最容易翻车的三个点
2.1 环境清单与版本匹配
先说必备组件。Flutter 鸿蒙开发不是装个 Flutter SDK 就能跑,你需要的是:
- Flutter SDK(选择支持 OpenHarmony 的分支或 3.27+ 的 release 版本)
- OpenHarmony SDK(建议直接用 DevEco Studio 自带的 SDK 包)
- DevEco Studio(主要用来创建鸿蒙工程、配置签名、连接真机)
- 鸿蒙真机或模拟器(模拟器配置比较重,真机反而简单)
版本匹配是这个环节最容易被忽视的问题。我最初直接用 Flutter 官方 stable 分支去跑鸿蒙,结果构建工具链直接报错。后来换成专门的 ohos 分支才正常。我的建议是:先去 GitHub 搜 flutter_flutter 的 ohos 分支,看它的 README 标注的 Flutter 版本和 OpenHarmony SDK 版本,严格按照对应关系安装,不要自己组合版本。
2.2 非华为电脑连接鸿蒙手机的完整链路
搜索"非华为电脑连接鸿蒙手机"的人特别多,这里把完整链路说清楚。连接鸿蒙真机的第一步不是开 USB 调试,而是先确认手机进入开发者模式:设置 → 关于平板/手机 → 连续点击版本号七次,激活开发者选项。
然后开启"USB 调试"和"仅充电模式下允许 ADB 调试"。注意,鸿蒙现在的默认 USB 连接方式可能是"仅充电",你需要在弹窗里手动切换到"传输文件"或"USB 调试"模式。用非华为电脑调试,关键在于 DevEco Studio 和 adb 工具的驱动。Windows 上很多时候是因为缺驱动导致 adb devices 列表空转,装一个通用的 USB 驱动就能解决。连上之后用命令验证:
adb devices如果能看到设备编号且状态为 device,就说明链路通了。如果显示 unauthorized,在手机上确认允许 USB 调试授权弹窗即可。整套步骤和 Android 调试几乎一致,遇到问题先往驱动和授权两个方向排查。
2.3 flutter create 之后跑不起来的排查思路
"flutter 新建项目后跑不起来"是个高频搜索词,我这次也踩到了。新建完项目直接用 Flutter run 跑鸿蒙设备,大概率会提示找不到设备或者缺少 SDK 配置。这里的关键是:Flutter run 默认只识别 Android 和 iOS 设备。要跑鸿蒙,你需要先用 DevEco Studio 打开 flutter create 生成的工程目录,完成鸿蒙侧的工程配置,然后才能通过 DevEco Studio 的 Run 按钮安装到真机,或者用命令行配合 ohos 构建脚本执行。
另一个坑是 gradle 配置。鸿蒙侧的构建链路是通过 DevEco 的 hvigor 构建工具串联的,和 Flutter 的 gradle 插件不完全一样。如果你在项目里看到类似 apply flutter's main gradle plugin 的报错,基本就是混用了两套构建体系。解决办法是严格按照 ohos 分支提供的模板来生成鸿蒙壳工程,不要自己往原生目录里塞 gradle 插件配置。
3. 宠物驱虫记录器的数据模型,先想清楚业务再写代码
3.1 需求拆解:驱虫记录到底要记录什么
宠物驱虫,尤其是猫狗,是有固定周期的。体内驱虫通常三个月一次,体外驱虫通常一个月一次。很多宠物主会忘记上次驱虫的时间,这款工具的核心场景就是:记录宠物档案、记录每次驱虫用药、自动推算下次驱虫时间、按宠物维度查看历史记录。
基于这个场景,我拆出了两类核心实体:
宠物基本信息:宠物名、品种、出生日期、体重、头像颜色标识
驱虫记录:宠物关联 ID、药品名称、驱虫类型(体内/体外)、用药量、驱虫日期、下次驱虫日期、备注
3.2 用 Dart 类建模,避免直接用 Map 传数据
很多初学者直接用 Map<String, dynamic> 存业务数据,短期的确方便,但项目一复杂就会出现字段名写错、类型不匹配、重构困难。这个项目里我把每个实体建模成一个 Dart 类,序列化和反序列化都写在类里面:
class Pet { final String id; final String name; final String breed; final DateTime birthDate; final double weight; Pet({ required this.id, required this.name, required this.breed, required this.birthDate, required this.weight, }); factory Pet.fromJson(Map<String, dynamic> json) { return Pet( id: json['id'] as String, name: json['name'] as String, breed: json['breed'] as String, birthDate: DateTime.parse(json['birthDate'] as String), weight: (json['weight'] as num).toDouble(), ); } Map<String, dynamic> toJson() { return { 'id': id, 'name': name, 'breed': breed, 'birthDate': birthDate.toIso8601String(), 'weight': weight, }; } }注意一个细节:weight 从 JSON 读出来的时候,用 (json['weight'] as num).toDouble(),因为 JSON 里的数值可能是 int 也可能是 double,直接 as double 会抛类型转换异常。这是我项目测试时实际遇到的坑。
3.3 存储方案选型:本地 JSON 文件就够了
宠物驱虫记录器的数据量非常小,不需要上 SQLite。我尝试过的方案里有 sqflite(需要原生插件适配)、Hive(纯 Dart 实现)、shared_preferences(轻量键值存储)。在鸿蒙上的实测结果是:sqflite 的鸿蒙适配还不太成熟,容易在插件注册环节出问题;Hive 依赖的纯 Dart 库在鸿蒙上可以正常运行。
不过最终我用的是最朴素的方式——把序列化后的 JSON 字符串通过 path_provider 写入应用私有目录。原因很简单:数据量小、结构简单、可读性好,而且避免引入额外依赖。鸿蒙的 path_provider 同样存在适配问题,我最后是用 flutter 的 dart:io 的 Directory 获取应用文档目录来替代。这里给一个实用判断标准:如果你的数据模型少于两个关联实体,直接用 JSON 文件存储;超过两个实体并且有查询需求,再考虑引入数据库。
3.4 驱虫周期计算逻辑
推算下次驱虫日期是核心功能,逻辑不复杂,但边界条件要考虑清楚。体内驱虫和体外驱虫的周期不同,我把它抽象成一个简单的计算函数:
DateTime calculateNextDate(DateTime baseDate, DewormType type) { final days = type == DewormType.internal ? 90 : 30; return baseDate.add(Duration(days: days)); }要注意的是:Duration(days: 90) 是严格的"90 个 24 小时",如果用户跨夏令时或者时区变化,日期可能偏差一天。更稳妥的做法是用 DateTime 的年月日做加法:
DateTime calculateNextDate(DateTime baseDate, DewormType type) { final monthOffset = type == DewormType.internal ? 3 : 1; return DateTime(baseDate.year, baseDate.month + monthOffset, baseDate.day); }这个方式直接按月份加,不会出现小时偏移的问题。真正的坑在月底:如果上次驱虫是 1 月 31 日,加一个月会变成 3 月 3 日(因为 2 月没有 31 号)。我处理的方式是:如果目标月份天数不足,自动取该月最后一天。这个逻辑虽然简单,但没有边界测试的话很容易翻车。
4. 用 Provider 管理驱虫记录的状态,响应式更新的核心逻辑
4.1 为什么选 Provider 而不是 Riverpod 或 Bloc
Flutter 状态管理的方案多到让人选择困难。我的选型逻辑很简单:项目小、依赖少、团队都懂、鸿蒙适配风险低。Provider 是基于 InheritedWidget 封装的官方推荐方案,不需要代码生成,不需要额外的编译步骤,在鸿蒙上的适配风险最小。Riverpod 很强大但引入的概念更多,Bloc 适合大型团队但样板代码太多。对于一个宠物驱虫记录器这种规模的项目,ChangeNotifier + Provider 是最务实的选择。
4.2 ChangeNotifier 与 Provider 的基础用法
我把数据存储和状态管理整合到一个 PetProvider 里,通过 ChangeNotifier 通知界面刷新:
class PetProvider extends ChangeNotifier { List<Pet> _pets = []; List<DewormRecord> _records = []; List<Pet> get pets => List.unmodifiable(_pets); List<DewormRecord> get records => List.unmodifiable(_records); void addPet(Pet pet) { _pets.add(pet); _save(); notifyListeners(); } List<DewormRecord> getRecordsForPet(String petId) { return _records.where((r) => r.petId == petId).toList(); } void addRecord(DewormRecord record) { _records.add(record); _save(); notifyListeners(); } Future<void> _save() async { // 序列化并写入文件 } }注意几个设计细节:对外暴露的 pets getter 用 List.unmodifiable 包裹,防止外部直接修改列表而绕过 notifyListeners;addRecord 和 addPet 内部统一调用 _save,保证持久化和状态更新不脱节。这些都是多踩几次坑才能沉淀出来的写法。
4.3 在界面层监听数据变化
在使用侧,最标准的做法是 Consumer 或 context.watch。比如宠物列表页:
@override Widget build(BuildContext context) { final petProvider = context.watch<PetProvider>(); final pets = petProvider.pets; return ListView.builder( itemCount: pets.length, itemBuilder: (context, index) { final pet = pets[index]; return ListTile( title: Text(pet.name), subtitle: Text('${pet.breed} · ${pet.weight}kg'), trailing: Text('记录 ${petProvider.getRecordsForPet(pet.id).length} 次'), onTap: () => Navigator.push(...), ); }, ); }context.watch () 会在 Provider 发出通知时自动重建当前 widget,这是 Provider 最方便的地方——不需要手动管理订阅和取消订阅。我在表单提交后会调用 provider.addPet 并立即看到列表更新,完全不需要手动 setState。
4.4 持久化与 Provider 的结合方式
启动时读取数据、操作时写入数据,这是持久化结合状态管理的最简模型。我在 Provider 构造函数里加了一个异步初始化方法:
class PetProvider extends ChangeNotifier { PetProvider() { _load(); } Future<void> _load() async { final data = await _readFromFile(); _pets = ...; _records = ...; notifyListeners(); } }调用侧用 ChangeNotifierProvider.value 或者 MultiProvider 挂载 Provider,然后在 runApp 之前先 await 初始化完成。这个环节有一个常见的坑:如果读取文件耗时长,界面会先闪一下空列表,体验不好。我的解决方式是加一个 Splash 页面,等 Provider 初始化完成后再进入主页面。
5. Flutter 组件通信的实战方式,以及这个项目用到了哪些
5.1 组件通信的本质:数据在组件层级间的流转
Flutter 的组件通信核心是"数据从哪来、变化怎么通知"。最基础的是父传子,通过构造函数把数据传给子组件;子传父则需要回调。跨多级页面时,就需要状态管理容器来架一座桥。宠物驱虫记录器这个项目里,三个场景全部涉及了。
5.2 构造函数传参:父传子的最直接路径
比如宠物卡片组件,我通过构造函数传入 Pet 对象和点击回调:
class PetCard extends StatelessWidget { final Pet pet; final int recordCount; final VoidCallback onTap; const PetCard({ required this.pet, required this.recordCount, required this.onTap, }); @override Widget build(BuildContext context) { return Card( child: ListTile( title: Text(pet.name), subtitle: Text('$recordCount 条驱虫记录'), onTap: onTap, ), ); } }父组件在使用时传入数据:
PetCard( pet: pet, recordCount: petProvider.getRecordsForPet(pet.id).length, onTap: () => Navigator.pushNamed(context, '/detail', arguments: pet.id), )这种写法最简单,但要注意:一旦 PetCard 的父级重建,整个 PetCard 也会跟着重建。如果 PetCard 内部有很多子组件,可以用 const 构造和 RepaintBoundary 优化,但在这个项目里没必要过度设计。
5.3 跨页面通信,通过 Provider 而不是路由参数堆砌
查看某只宠物的记录详情页时,需要把宠物的 ID 传过去。经典的做法是 Navigator.push 的 arguments 传递。但这个项目的详情页不仅展示宠物信息,还需要显示该宠物名下的所有驱虫记录,甚至支持在详情页内新增记录。如果全靠路由参数传,会越传越乱。我的做法是:路由只传宠物 ID,数据全部从 PetProvider 中读取。
这样的好处是,详情页内新增驱虫记录后,不需要通过返回值回传列表页,Provider 的数据更新会自动让列表页的 Consumer 重建。这就是状态管理容器带来的通信简化——跨页面通信不再是一层一层地传值,而是所有页面共享同一个数据源。
5.4 表单与列表的通信:回调 + Provider 双管齐下
新增宠物的表单页面需要把填好的数据交还给 Provider。这里用到了两种通信方式:
- 表单页通过 Navigator.pop 把新宠物对象返回给上一页
- 上一页拿到对象后调用 provider.addPet,触发全局通知
具体代码:
final newPet = await Navigator.push<Pet>( context, MaterialPageRoute(builder: (_) => AddPetPage()), ); if (newPet != null && context.mounted) { context.read<PetProvider>().addPet(newPet); }这个模式清晰且符合直觉。关键细节是:Navigator.push 返回 Future,用 await 等待结果,拿到非空结果后才写入 Provider。这里一定要判断 context.mounted,否则在异步回调里使用 context 会触发 Flutter 的 use_build_context_synchronously 警告,这在 Flutter 3.7 之后是硬性的 lint 规则。
6. 鸿蒙真机适配记录,从布局差异到构建链接
6.1 避开 Flutter 布局组件的平台差异假设
Flutter 的布局系统是自绘的,理论上在所有平台表现一致。但我实测下来,鸿蒙上有几个组件需要注意:
- RelativeContainer: 这个热搜词反映的是鸿蒙原生的布局容器,但 Flutter 里没有直接对应的组件。如果你在网上搜到鸿蒙布局教程,看到 RelativeContainer 的用法,不要试图在 Flutter 里找同名组件,Flutter 用的是 Stack + Positioned 来实现相对定位。
- Flex: Flutter 的 Flex 组件在鸿蒙上表现正常,但 Row、Column 的 MainAxisAlignment 和 CrossAxisAlignment 在不同屏幕上表现会有差异,尤其是 SafeArea 的处理会比 Android 更严格,底部导航需要额外预留手势条空间。
- Tabs: Flutter 的 TabBar 在鸿蒙上没问题,但要注意 TabController 的 lifecycle 管理。鸿蒙的后台进程回收策略和 Android 不太一样,重新进入页面时 TabController 的状态可能被重置。解决方案是在 State 的 initState 里重新创建 controller,或者在 didChangeAppLifecycleState 里做恢复。
6.2 Impeller 渲染引擎在鸿蒙上的表现
"flutter impeller"是 Flutter 渲染引擎的新方向,默认在 iOS 上启用,Android 上逐步推进。在鸿蒙的 ohos 分支上,我测试的结果是:Impeller 模式可以开启,但部分场景存在兼容性问题,具体表现为某些复杂的圆角裁剪渲染异常。如果你的应用大量使用 ClipRRect 或者自定义阴影,建议先保留默认渲染后端,等适配稳定后再切 Impeller。
实际上对于宠物驱虫记录器这种界面组件比较常规的应用,渲染性能完全取决于 Dart 代码本身的效率,渲染引擎的差异感知不强。纠结 Impeller 有没有启用,不如关注列表项构建时的重建频率。
6.3 构建 HAP 包与真机安装的完整流程
从 Flutter 工程到可安装的鸿蒙 HAP 包,我测试下来的完整流程如下:
用 DevEco Studio 打开 Flutter 工程下的鸿蒙壳目录(如果 ohos 分支的模板正确生成,会有独立的 ohos 目录)。等待 DevEco 索引完成,配置签名(测试阶段使用自动签名即可,不需要购买证书)。点击 Build 或直接点 Run 安装到已连接的鸿蒙真机。
如果通过命令行构建,可以进入 ohos 目录执行:
hvigorw assembleHap构建完成后的 HAP 包路径一般在 ohos/entry/build/default/outputs/default/ 下,可以通过 DevEco Studio 的 Device 面板直接安装,也可以用 hdc 命令安装:
hdc install entry-default-signed.haphdc 是鸿蒙的调试工具,类似 Android 的 adb。实测下来 hdc install 成功率比 DevEco 的拖拽安装更稳定,尤其是包路径比较深的时候。
6.4 真机调试时最容易忽略的授权问题
鸿蒙的隐私权限比 Android 严格,虽然宠物驱虫记录器不需要网络权限,但如果你的应用日后要加云同步功能,需要在鸿蒙侧的 module.json 配置文件里声明 ohos.permission.INTERNET。另外,读取本地文件如果是从应用私有目录读取,不需要额外权限;但如果要访问公共存储目录,就必须申请 ohos.permission.READ_MEDIA 之类的权限。这个权限声明和 Android 的 AndroidManifest 是两套体系,不要搞混。
7. 上线前的清单检查,以及我留给后续开发者的三个建议
7.1 测试用例覆盖边界:驱虫日期计算最值得测
宠物驱虫记录器的核心算法是日期计算,我建议至少覆盖这四类用例:普通日期加一个月;月底日期加一个月;跨年日期加三个月;闰年二月加一个月。我写了一个简单的手动测试函数,直接在 main 里跑断言,比 UI 手动点击高效得多。如果你日后把日期计算改成用 time_machine 之类的库,这些断言可以直接复用。
7.2 鸿蒙键盘遮挡输入框的适配
真机测试时发现,鸿蒙软键盘弹出后,底部的输入框会被键盘遮挡。Flutter 默认的 Scaffold resizeToAvoidBottomInset 在 Android 上表现正常,但我在鸿蒙上遇到过不生效的情况。解决方案是把表单页面包一层 SingleChildScrollView,并手动指定 padding 为 mediaQuery.viewInsets.bottom。实测在鸿蒙上这个方案比较稳定。
7.3 我的三个建议,基于这次实战的经验教训
版本锁定要趁早。Flutter 和 OpenHarmony 的 SDK 版本对应关系比较严格,项目一启动就锁版本,不要总想着"升到最新",鸿蒙适配的节奏和官方渠道是两个不同的更新频率。
优先跑通最小闭环。不要一开始就想着做完所有功能,先把空壳工程跑到鸿蒙真机上,确认构建链路通畅,再逐步加入业务功能。我就是先写了一个 Hello World 页面,跑通了才继续开发的。
保留一个备用 Android 设备。Flutter 鸿蒙开发的过程中,很多问题需要排除法是 Flutter 本身的问题还是鸿蒙适配的问题,这时候用 Android 设备跑同一段代码就能快速定位。跨平台开发的调试思路永远是:先确认平台无关层正常,再排查平台相关层。
我在这个项目里最大的体会是:跨平台开发的价值不在于"一次编写,处处运行"这句口号,而在于业务逻辑、数据模型和状态管理这三大块真正做到平台无关。宠物驱虫记录器的 Dart 代码,从数据序列化到 Provider 状态管理,没有一行和鸿蒙或 Android 强相关,这才是 Flutter 在鸿蒙开发中真正的护城河。