前阵子团队接到一个需求,要做一个宠物日常记录类App,覆盖Android和iOS还不够,老板补了一句“鸿蒙也得能跑”。第一反应是头大,传统思路要么拆三套原生团队,要么用ArkTS重写一遍,成本直接翻倍。但我自己一直是Flutter的重度用户,所以花了几天时间把Flutter拉上鸿蒙的适配链路整体验了一遍——最后确认,Flutter完全可以在鸿蒙设备上跑通完整业务。这篇就记录一下我用Flutter开发一款宠物日常记录应用的全过程,包含项目架构、核心功能实现、鸿蒙适配细节、打包流程,以及我在实际操作中踩过的坑。
无论你是准备做跨平台选型的技术负责人,还是想低成本把手上的Flutter应用移植到鸿蒙的开发者,这篇都有参考价值。整个工程麻雀虽小五脏俱全:宠物档案、喂食/遛弯/疫苗提醒、统计图表、照片存储,基本的典型业务全都覆盖了,而且不需要额外引入重型服务端,纯本地就能跑。建议跟着流程实际操作一遍,动手一次比看十篇文档都强。
我之前发现很多人卡在环境配置,还没走到写业务代码就放弃了。所以这篇把环境搭建的过程拆得特别细,连版本匹配、环境变量都写了。到后面讲到功能实现时我会直接贴代码讲解,最后整理了一份问题排查清单,几乎覆盖了我自己遇到的、以及身边朋友反复踩的坑。
1. 项目启动:为什么选Flutter做鸿蒙开发
1.1 跨平台方案的技术判断
宠物日常记录应用听起来简单,但实际功能拆开并不少:宠物档案管理、多条记录类型、图片上传、通知提醒、数据统计。如果按传统路线走,Android、iOS各写一套原生,鸿蒙再单独写一套ArkTS,三套代码的维护成本会把小团队直接拖垮。这个项目选型时,我对比过几个方案:
- ArkTS纯原生开发:性能和系统能力调用最理想,ArkUI的声明式写法也现代,但问题是一套代码只能在鸿蒙上跑,Android/iOS还得另起炉灶。
- React Native + 鸿蒙适配层:社区有一些适配方案,只是第三方库的鸿蒙兼容性断档比较明显,很多原生模块要自己补。
- Flutter + OpenHarmony SDK分支:Flutter自绘引擎理论上可以做到UI层完全一致,业务层一套代码,只需处理少量平台通道差异。
最后我选了Flutter,原因有三个:一是Flutter的渲染引擎不依赖系统自带控件,适配新系统时UI层不用大改;二是Dart语言的AOT编译为机器码之后,性能在宠物记录这种中低复杂度场景下绰绰有余;三是Flutter社区生态成熟,日期选择、图表、图片处理等常见需求都有现成库。
选型时还有一个隐性考量:人才储备。团队里不止我一个人熟悉Flutter,如果选冷门方案,招人都是难题。而目前招聘市场上Flutter的面试题和岗位需求已经相当普及,后续扩容团队成员也相对容易。
1.2 环境准备:Flutter与鸿蒙SDK的安装与版本匹配
鸿蒙Flutter开发不等于直接用官方Flutter主线,而是要使用OpenHarmony组织的Flutter分支。我踩过最大的坑就是版本匹配问题——Flutter版本、OpenHarmony SDK版本、编译打包工具链三者必须对齐,否则编译报错会让你怀疑人生。
先说我的推荐组合(这个搭配实测最稳):
| 组件 | 推荐版本 | 备注 |
|---|---|---|
| Flutter SDK | 3.16.9(OpenHarmony分支) | 3.16版本对API 9支持比较成熟 |
| Dart SDK | 3.2.x(随Flutter自带) | 不需要单独安装 |
| OpenHarmony SDK | API 9(4.0.10.10以上) | API 10也能用,但部分组件需额外适配 |
| DevEco Studio | 4.0 Release | 用于编译和签名 |
| 编译目标 | HAP(鸿蒙应用包) | 真机调试需要开发者证书 |
安装步骤我直接按顺序走一遍:
-
下载OpenHarmony分支的Flutter SDK。这里要特别强调:不要用google官方路径的Flutter SDK,编译鸿蒙时会直接报错找不到platform。正确做法是拉取OpenHarmony/flutter_flutter仓库的对应release分支。
-
下载DevEco Studio并安装,它会附带OpenHarmony SDK。如果你的电脑已经装了Android Studio,两者可以共存,端口不冲突。
-
配置环境变量。需要设置
ANDROID_HOME(部分插件依赖)、DEVECO_SDK_HOME指向DevEco安装目录下的Sdk文件夹。 -
在Flutter SDK目录下执行
flutter config --enable-ohos点亮鸿蒙平台支持,然后运行flutter doctor检查环境。正常输出里应该能看到OHOS toolchain相关条目。
我建议把这一步截图保存,后续排查时对照检查会方便很多。
1.3 创建并跑通第一个鸿蒙Flutter工程
环境配好之后,创建工程的方式和Android/iOS略有不同。关键点是:flutter create之后,默认只会生成android、ios目录,你需要额外运行一次命令让Flutter为鸿蒙平台生成ohos目录。
我当时的操作流程是这样:
bash复制# 创建项目,org名按自己公司域名反写
flutter create --org com.example --project-name pet_diary pet_diary
# 进入项目目录
cd pet_diary
# 生成鸿蒙平台目录
flutter create --platforms ohos .
执行完之后,项目根目录会出现一个ohos文件夹,这就是鸿蒙工程的入口。它对应Android工程里的android目录,后续打包、签名都要用到。
第一件事不是写业务代码,而是先跑一个空项目验证链路。打开DevEco Studio加载ohos目录,连接鸿蒙真机或启动模拟器,直接点运行。如果一切顺利,屏幕上应该出现默认的Flutter计数器Demo界面。
很多人在这一步就停下不走了,实际上后续的复杂度主要在业务实现和平台差异处理上,基础跑通之后反而是一马平川。接下来进入正题。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 应用整体架构与数据流设计
2.1 功能模块划分
宠物日常记录应用虽然业务不复杂,但功能模块还是得提前划分清楚,否则写到后面代码会绕成一团。我按数据维度和用户操作维度把整个应用拆成了四个核心模块:
- 宠物档案模块:维护宠物基本信息(昵称、品种、生日、性别、体重)和头像照片。一个用户可以添加多只宠物,这是所有后续记录的父级维度。
- 日常记录模块:核心功能,按类型记录事件,包括喂食、遛弯、洗澡、便便、睡眠、用药六种。每条记录带时间戳、备注、可选照片。
- 记录时间线模块:按日期倒序展示所有事件的流式页面。支持按宠物筛选、按记录类型筛选。
- 统计与提醒模块:统计近30天各类型事件的频次分布,用图表展示;为周期性的需求(疫苗、驱虫药)设置提醒通知。
这个划分方式和很多工具类App是一致的:先确定核心数据模型,再为它设计录入、展示、统计三个出口。架构上不做过度设计,单机应用就用Provider做状态管理、Hive做本地存储,不引入网络层,逻辑会非常清爽。
2.2 本地存储方案选型
做本地数据存储,Flutter生态里主要有三个选择:shared_preferences、sqflite、hive。我最终选了Hive,理由很实际:
shared_preferences只适合存键值对这类轻量配置,存宠物列表和记录列表这种结构化数据,序列化和反序列化的心智负担太重。sqflite在Android/iOS上很成熟,但鸿蒙适配层还不太完善,编译时容易遇到原生模块找不到的问题,对鸿蒙Flutter适配来说还要额外处理不少边界问题。hive是纯Dart实现的NoSQL数据库,不依赖原生代码,这决定了它在鸿蒙上的兼容性几乎不用操心。而且它支持对象直接存储,写入速度快,对宠物记录这种频繁追加、偶尔查询的场景非常合适。
数据层的封装我写得比较朴素,核心就三步:初始化Hive、注册适配器、打开Box。我把操作封装成一个单例服务,业务层不用关心底层存储细节。
dart复制// 数据模型:宠物档案
@HiveType(typeId: 0)
class PetModel extends HiveObject {
@HiveField(0)
String name;
@HiveField(1)
String breed;
@HiveField(2)
DateTime birthday;
@HiveField(3)
String avatarPath;
@HiveField(4)
double weight; // 单位 kg
PetModel({
required this.name,
required this.breed,
required this.birthday,
required this.avatarPath,
required this.weight,
});
}
// 数据模型:日常记录
@HiveType(typeId: 1)
class RecordModel extends HiveObject {
@HiveField(0)
String petKey;
@HiveField(1)
String type; // feed / walk / bath / poo / sleep / medicine
@HiveField(2)
DateTime occurredAt;
@HiveField(3)
String note;
@HiveField(4)
String photoPath;
RecordModel({
required this.petKey,
required this.type,
required this.occurredAt,
required this.note,
required this.photoPath,
});
}
使用@HiveType标注的类型,需要给每个类生成一个TypeAdapter,这样Hive才能把对象序列化存储。这一步很容易漏,编译时不一定报错,但运行时写入数据库会抛异常。
生成适配器用build_runner:
bash复制flutter pub run build_runner build --delete-conflicting-outputs
“--delete-conflicting-outputs”这个参数很关键,不加的话增量构建经常会因为旧文件冲突而失败。
2.3 状态管理与依赖注入
宠物记录应用的状态量不算多:当前选中的宠物、记录列表、筛选条件、UI主题。用Provider做状态管理足够。我习惯按功能拆分ChangeNotifier,而不是所有状态塞一个类里:
PetProvider:管理宠物列表的增删改查,以及当前选中宠物。RecordProvider:管理当前宠物的记录列表,提供按条件筛选的方法。ReminderProvider:管理提醒配置和通知触发。
这样拆的好处是不同模块的刷新时机互不干扰。比如添加一条新记录时,只需要通知RecordProvider更新,PetProvider不会跟着重建页面,避免无谓的UI刷新。
Dart的异步事件通过Stream或Future处理。宠物记录操作基本都是写本地数据库,很快,不需要引入rxdart这类重库。记笔记和选择时间用的是表单页,提交时await数据库写入,完成后Navigator.pop返回列表页刷新,逻辑非常直白。
3. 核心功能实现细节
3.1 宠物档案管理:模型、表单、头像处理
宠物档案页是整个应用的数据入口,没有宠物信息,后续所有记录都无从谈起。我在这一步实现了三个操作模式:新增、编辑、选择当前宠物。
先说一下表单的结构。宠物信息就四个字段:昵称、品种、生日、体重。用Flutter的Form组件包起来,配合TextFormField做必填校验。生日字段用showDatePicker做选择,体重用数字键盘。
dart复制// 新增宠物页面核心逻辑
class AddPetPage extends StatefulWidget {
final PetModel? existingPet; // 为空表示新增,不为空表示编辑
const AddPetPage({super.key, this.existingPet});
@override
State<AddPetPage> createState() => _AddPetPageState();
}
class _AddPetPageState extends State<AddPetPage> {
final _formKey = GlobalKey<FormState>();
late final TextEditingController _nameController;
late final TextEditingController _breedController;
late final TextEditingController _weightController;
DateTime? _birthday;
String? _avatarPath;
@override
void initState() {
super.initState();
_nameController = TextEditingController(text: widget.existingPet?.name ?? '');
_breedController = TextEditingController(text: widget.existingPet?.breed ?? '');
_weightController = TextEditingController(
text: widget.existingPet?.weight.toString() ?? '');
_birthday = widget.existingPet?.birthday;
_avatarPath = widget.existingPet?.avatarPath;
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: Text(widget.existingPet == null ? '添加宠物' : '编辑宠物')),
body: Form(
key: _formKey,
child: ListView(
padding: const EdgeInsets.all(16),
children: [
// 头像选择
Center(
child: GestureDetector(
onTap: _pickAvatar,
child: CircleAvatar(
radius: 48,
backgroundImage: _avatarPath != null
? FileImage(File(_avatarPath!))
: null,
child: _avatarPath == null ? const Icon(Icons.pets) : null,
),
),
),
TextFormField(
controller: _nameController,
decoration: const InputDecoration(labelText: '昵称'),
validator: (value) =>
value == null || value.isEmpty ? '请填写宠物昵称' : null,
),
// ... 品种、生日、体重字段类似,这里省略
],
),
),
);
}
}
这里最需要留意的是图片选择。宠物头像、照片记录都涉及访问系统相册。在Android/iOS上,image_picker插件一行代码就能搞定,但鸿蒙上直接用会踩坑——image_picker对OpenHarmony的适配不完善,我后面会专门讲怎么通过平台通道调用鸿蒙原生能力来解决。
3.2 记录时间线:列表结构、筛选与按日分组
日常记录是整个应用的核心,我需要让用户最快速度完成“添加一条记录”这个动作。所以我把主页面设计成顶部是“快捷添加”按钮组(喂食、遛弯、洗澡、便便、睡眠、用药六个图标按钮),下面才是记录时间线。
记录时间线用ListView实现,数据按日期分组。我先把当天所有记录拉出来,按occurredAt倒序排列,然后按“今天”“昨天”“更早”三个维度分组展示。这个分组逻辑放在RecordProvider里,UI层只负责渲染。
dart复制// 按日期分组后的记录列表
Map<String, List<RecordModel>> groupRecordsByDate(List<RecordModel> records) {
final result = <String, List<RecordModel>>{};
for (final record in records) {
final dateKey = DateFormat('yyyy-MM-dd').format(record.occurredAt);
result.putIfAbsent(dateKey, () => []).add(record);
}
return result;
}
每组头部的日期单独占一行,下面才是当天的记录条目。这个设计比“每条记录都显示完整日期”清爽得多,用户往下滑的时候扫一眼就知道是哪天的记录。
另外,筛选操作我也放在了时间线页面。用ChoiceChip做筛选条件:全部 / 喂食 / 遛弯 / 洗澡 / 便便 / 睡眠 / 用药。选中不同芯片时,RecordProvider内部根据类型过滤之后重新返回列表。这里有个性能优化细节:筛选不需要重新查询数据库,而是在内存中过滤已加载的记录,响应速度会快很多。
3.3 统计与提醒:fl_chart图表与本地通知
统计模块我用了开源的fl_chart库。这个库的维护比较积极,而且纯Dart实现,鸿蒙适配没有障碍。我用它做了两个维度:
- 饼图:展示最近30天各类记录的比例,直观看出喂食、遛弯占比是否合理。
- 柱状图:展示最近7天每天新增记录数量的趋势。
fl_chart的写法需要把握好数据回调的结构:
dart复制// 饼图样例代码
PieChart(
PieChartData(
sections: [
PieChartSectionData(
value: feedCount.toDouble(),
title: '喂食$feedCount',
color: Colors.orange,
),
PieChartSectionData(
value: walkCount.toDouble(),
title: '遛弯$walkCount',
color: Colors.blue,
),
// ... 其他类型
],
),
)
提醒功能则是依赖系统通知。在Android上,需要申请POST_NOTIFICATIONS权限,然后通过flutter_local_notifications插件创建定时通知;在鸿蒙上,通知服务走了一条完全不同的调用路径,直接从sys.distributeddevice权限体系那边绕过去不太行,必须调用鸿蒙系统API。这里也需要平台通道。
4. 鸿蒙适配:平台通道与原生交互
4.1 图片选择:从image_picker到鸿蒙PhotoViewPicker
鸿蒙Flutter开发中最让人头疼的就是系统调用。以选择图片为例,image_picker插件在OpenHarmony上编译能过,但运行时点击头像选择时,大概率会直接闪退或者无响应。原因在于插件内部调用的是Android原生接口,鸿蒙系统的Activity和PhotoPicker模型与Android是完全不同的。
解决办法是走Platform Channel自己封装。Flutter侧定义好方法名,鸿蒙原生侧用ArkTS写对应的实现。我以“从相册选择一张图片并返回沙箱路径”为例,完整链路如下。
Flutter侧定义通道:
dart复制// 平台通道封装:选择图片
class OhosImagePicker {
static const MethodChannel _channel = MethodChannel(
'com.example.pet_diary/image_picker',
);
// 调用鸿蒙原生选择器
static Future<String?> pickImage() async {
try {
final String? path = await _channel.invokeMethod('pickImage');
return path;
} on PlatformException catch (e) {
debugPrint('调用鸿蒙图片选择器失败: ${e.message}');
return null;
}
}
}
鸿蒙侧实现(ArkTS):
ts复制// 鸿蒙原生侧注册平台通道
import { MethodChannel, MethodCall, Callback } from '@ohos.arkui.web';
const channel = new MethodChannel('com.example.pet_diary/image_picker');
channel.setMethodHandler('pickImage', (call: MethodCall, callback: Callback) => {
// 调用PhotoViewPicker选择图片
const photoPicker = new photoAccessHelper.PhotoViewPicker();
const result = await photoPicker.select({
MIMEType: photoAccessHelper.PhotoViewMIMETypes.IMAGE_TYPE,
maxSelectNumber: 1,
});
// 把图片复制到应用沙箱并返回路径
const uri = result.photoUris[0];
const file = fileIo.openSync(uri, fileIo.OpenMode.READ_ONLY);
// 省略复制逻辑,返回沙箱路径
callback.success(sandboxPath);
});
需要注意:鸿蒙系统里,应用访问相册里的图片受到严格的权限管控,直接拿Uri不代表可以持久使用。正确的姿势是把选中的图片复制到应用专属沙箱目录中,再在Flutter侧读取。如果只是保存一个系统相册的Uri,下次启动应用时读取大概率会失败。
4.2 通知权限申请与本地通知实现
宠物提醒功能在鸿蒙上的实现和Android差异也很大。Android的flutter_local_notifications插件直接失效,因为底层调用的是Android NotificationManager。鸿蒙这边我同样通过平台通道调用了系统通知能力。
鸿蒙的通知分为两种:普通通知和提醒通知。疫苗、驱虫这类定时提醒应该使用提醒通知(ReminderAgent),它允许应用在指定时间点发出系统级通知,还能设置重复周期。
ts复制// 鸿蒙侧创建定时提醒
import reminderAgentManager from '@ohos.reminderAgentManager';
const reminder = {
reminderType: reminderAgentManager.ReminderType.REMINDER_TYPE_TIMER,
triggerTimeInSeconds: 3600, // 1小时后提醒
content: '到了给宠物吃驱虫药的时间啦',
actionButton: [
{ title: '查看', type: reminderAgentManager.ActionButtonType.ACTION_BUTTON_TYPE_CLOSE },
],
wantAgent: {
pkgName: 'com.example.pet_diary',
abilityName: 'MainAbility',
parameters: { 'page': 'record' },
},
};
reminderAgentManager.publishReminder(reminder)
.then((reminderId) => {
// 保存 reminderId 用于取消提醒
});
Flutter侧只需要调用Platform Channel把这个定时器注册出去即可。这里的核心思路是:不要试图在Dart层做定时任务推通知,纯Dart的定时器在App进程被杀后就会失效,必须依赖系统级的提醒服务。
4.3 导航、窗口、权限适配的细枝末节
鸿蒙系统在页面导航、窗口尺寸、权限弹窗方面和Android存在差异,我在适配过程中总结出几个细节:
-
页面返回手势:鸿蒙的侧滑返回手势区域默认比Android宽,Flutter默认的手势处理会有点冲突,表现是侧滑时页面容易出现“卡在半路”的动画。解决方式是在MaterialApp的ThemeData里调整
pageTransitionsTheme,自定义一个兼容鸿蒙手势的过渡动画。 -
安全区适配:鸿蒙状态栏高度和Android的挖孔屏状态栏高度计算方式不同。我用
MediaQuery.of(context).padding.top获取的状态栏高度在真机上偏小,页面顶部内容会被状态栏遮挡。后来改为读取鸿蒙系统参数,通过平台通道获取真实安全区距离。 -
权限弹窗时序:鸿蒙的相机、相册、通知权限都是首次调用时弹出。如果用户在权限弹窗出现前就触发了UI动画,偶尔会出现弹窗被UI覆盖的情况,表现为“点了没反应”。我的做法是在进入需要权限的页面之前,先通过一个
PermissionService统一预申请权限,页面渲染完成后再调用系统能力。 -
图片缓存目录差异:鸿蒙的沙箱路径与Android不同。Android的
getFilesDir()在鸿蒙上不能用,Flutter的path_provider插件虽然能返回一个路径,但指向的目录在鸿蒙的沙箱体系下并不一定可写。我的方案是统一用平台通道从鸿蒙侧获取应用缓存目录,再拼上相对路径。
这些细节单拎出来任何一个都看似微不足道,凑在一起就是鸿蒙Flutter开发新手最容易卡壳的地方。建议在开发初期就用真机多跑几个页面,比到最后一口气适配效率高得多。
5. 打包发布与性能优化
5.1 鸿蒙应用打包签名完整流程
Flutter项目在鸿蒙上打包时,不是执行flutter build apk,而是要先用Flutter编译产物,再通过DevEco Studio打包HAP。
我直接说实操步骤。
第一步,执行Flutter命令生成鸿蒙平台的构建产物:
bash复制flutter build ohos --release --target-platform ohos-arm64
这个命令会在build/ohos/release目录下生成libflutter.so、libapp.so等文件。如果只构建不打包,这些就是最终产物。
第二步,打开DevEco Studio,加载项目ohos目录,在File > Project Structure > Signing Configs里配置签名。鸿蒙的签名逻辑和Android类似,需要一个证书文件和Profile文件。这两个文件要在AppGallery Connect后台申请。
第三步,在DevEco Studio里选择Build > Build Hap(s)/APP(s) > Build Hap(s)。构建成功后会在ohos/build/default/outputs/default/下生成.hap文件,这就是可以直接安装到鸿蒙手机上的应用包。
如果只是跑真机调试,其实不需要走这么复杂的签名流程。DevEco Studio的“自动签名”功能会自动申请调试证书,点一下就能跑。但要注意,调试证书和应用正式签名不是一回事,上线必须要单独的发布证书。
5.2 包体大小与启动性能优化
Flutter在鸿蒙上的包体积和启动速度这两个指标,直接关系到用户第一印象。我的应用发布包大约20MB,对工具类应用来说完全可以接受,如果你想进一步压缩,有几个方向:
- 裁剪字体:如果应用只显示中文和英文,使用
google_fonts时只打包需要的字重,不要全部引入。 - 开启tree-shaking:release构建默认开启,部署时要留意不要误关闭。
- 压缩图片资源:启动页、空状态插画这类静态图片压缩成WebP,体积能减少60%以上。
启动性能方面,宠物记录应用冷启动时间实测在900ms左右,主要耗时在Hive初始化。我做了两个优化:一是把依赖的初始化放到异步SplashScreen阶段,二是用PathProvider缓存常用数据。从用户点击图标到看到主页面的整体体感比优化前提升了约30%。
5.3 真机调试的几个效率技巧
鸿蒙Flutter开发调试和对比,最大的痛点就是热重载不像Android/iOS那样稳定。我实测下来,改动Dart代码后执行Hot Reload成功率还算可以,但偶尔会出现状态丢失或者页面白屏。如果遇到这种情况,不要反复尝试热重载,直接冷启动一次,反而更省时间。
另一个提高效率的点是日志输出。鸿蒙的日志系统和Android的Logcat不通用,Flutter的debugPrint在release包不会输出,但在debug包可以看到。我习惯在关键业务节点用debugPrint('[$tag] $message')的形式打日志,排查问题时直接按tag过滤,效率非常高。
6. 常见问题与排查技巧实录
6.1 编译报错:插件原生库找不到
这是我在鸿蒙Flutter开发中遇到最多的错误。具体表现是编译时报Class not found或Symbol not found,通常指向某个第三方Flutter插件的原生实现。
排查思路:
- 检查该插件是否有OpenHarmony分支。很多主流插件已经支持鸿蒙,但需要你显式在
pubspec.yaml里指定git源,而不是用pub.dev的官方源。 - 如果插件没有鸿蒙适配,看它是否纯Dart实现。纯Dart插件直接能用,不用处理。
- 插件的原生代码是Android的但未适配鸿蒙,就得用我前面提到的方式通过Platform Channel自行调用鸿蒙API。
建议优先选择纯Dart实现的插件,这是从根源上降低鸿蒙适配成本的最有效方法。
6.2 真机运行报:路径或目录不存在
鸿蒙沙箱目录和Android不同,很多Flutter插件默认获取文件目录的方法在鸿蒙上会失效。
解决方案:在鸿蒙原生侧通过context获取应用专属目录,通过平台通道传给Flutter侧。我封装了一个PathService,统一管理数据库文件、图片缓存、日志文件的根目录。
dart复制// Flutter侧获取沙箱根目录
final String? sandboxRoot = await MethodChannel(
'com.example.pet_diary/path').invokeMethod('getSandboxPath');
6.3 提醒通知不触发
我遇到过一个诡异的问题:代码里明明调用了鸿蒙的提醒接口,也返回了reminderId,但到了设定时间就是不弹出通知。排查了一圈,发现问题是出在鸿蒙的“通知权限”默认是关闭的,用户首次打开App时弹出的权限申请用户点了“禁止”,之后不会自动开启。
解决思路:
- 在提醒页面增加一个“通知权限引导”的入口,检测到通知权限关闭时,引导用户去系统设置里打开。
- 设置提醒成功之后,立即发一条测试通知,确认系统级的通知链路是通的。
还有一个坑是时区。鸿蒙提醒使用的时间戳是UTC+8还是本地时间,不同版本API表现还不一样。为了稳妥起见,我在设置提醒时统一传UTC时间戳,然后让系统按本地时区换算触发时间。
6.4 第三方Flutter库兼容性速查表
为了方便后来者,我把宠物记录App中用到的所有Flutter依赖及其鸿蒙适配情况整理成一张表,如果你也在做类似项目,可以直接参考:
| 依赖库 | 用途 | 鸿蒙兼容情况 | 建议方案 |
|---|---|---|---|
| hive | 本地数据库 | 完全兼容(纯Dart) | 直接使用 |
| provider | 状态管理 | 完全兼容(纯Dart) | 直接使用 |
| fl_chart | 统计图表 | 完全兼容(纯Dart) | 直接使用 |
| intl | 国际化/格式化 | 完全兼容(纯Dart) | 直接使用 |
| image_picker | 图片选择 | 不兼容 | 使用Platform Channel调用鸿蒙PhotoViewPicker |
| path_provider | 沙箱目录 | 部分兼容,路径体系不同 | 自行封装目录获取 |
| flutter_local_notifications | 本地通知 | 不兼容 | 使用鸿蒙ReminderAgent代替 |
| cached_network_image | 网络图片缓存 | 需要测试 | 纯Dart版本可用,如果涉及原生缓存则需适配 |
这张表是我实测得出的结论,不过插件版本更新很快,你真正上手时最好先查一下插件官方仓库是否已新增鸿蒙支持。
6.5 从“跑通”到“跑稳”的调试经验
最后分享一个我自己养成的习惯:每写完一个功能模块,在Android真机和鸿蒙真机各跑一遍全流程测试,而不是攒到最后统一测。因为Flutter在Android和iOS上行为高度一致,但鸿蒙因为平台差异,容易出现“Android好好的,鸿蒙就崩了”的情况。提前做交叉测试,能帮你把平台差异的坑分散到一个一个模块里去解决,而不是最后集中爆发。
另外,我强烈建议在开发阶段使用鸿蒙官方模拟器跑真机场景测试,覆盖低电量、弱网(本地单机App弱网这块反而影响不大)、后台切换这些场景的稳定性。模拟器无法完全替代真机,但至少能帮你提前暴露一部分问题。
我在这个项目中还有一个体会:鸿蒙Flutter开发目前仍属于“先驱者”阶段,遇到问题时搜索引擎能搜到的答案有限,所以自己动手Debug的能力比任何框架知识都重要。多看看鸿蒙的官方API文档,再结合Flutter的报错日志去反推问题,基本上能解决90%的困难。剩下10%解决不了的问题,就去对应插件仓库提Issue,把报错信息、复现步骤、系统版本完整贴出来,一般维护者都能很快回复。
