去年春天,我一边打喷嚏一边盯着天气 App 发呆:空气质量有指数,花粉过敏人群最关心的花粉浓度却只能靠猜。那阵子我刚把手头的 Flutter 框架跨平台鸿蒙开发项目跑通,干脆把“实时花粉浓度查询”做成一个智能过敏防护助手,自己用得上,也能顺带验证 Flutter 在鸿蒙设备上的落地能力。这个项目最终不只解决了我个人的需求,还让我把数据链路、鸿蒙适配、通知提醒和真机调优整个走了一遍。如果你也想做天气、健康或者物联网类跨平台应用,又恰好要覆盖鸿蒙设备,这篇文章应该能帮你少踩不少坑。
1. 为什么把花粉浓度查询做成 Flutter + 鸿蒙?——跨平台选型的真实原因
1.1 需求场景:花粉过敏的人真正缺什么
花粉过敏的麻烦在于它有滞后性。等你眼睛痒、连续打喷嚏的时候,浓度其实已经高了一段时间了。天气预报只能告诉你今天多少度、下不下雨,不会告诉你“现在树粉浓度正在快速上升,出门请戴口罩”。我自己的需求非常具体:打开手机,第一屏告诉我今天要不要防护;第二屏告诉我未来几小时趋势;第三屏才能是详细数据。
当时身边的家人朋友用的手机很杂,有 iPhone、有 Android、也有几台鸿蒙设备。如果只做一个鸿蒙原生应用,覆盖面太窄;如果只做 Web,后台定位和通知推送又很别扭。所以我给自己定的目标是:一套 Flutter 业务代码,同时覆盖 Android、iOS 和鸿蒙,重点优先把鸿蒙跑通。这也是“Flutter 框架跨平台鸿蒙开发”这个方向最吸引人的地方——理论上写一次 UI 和业务逻辑,三端都能用。
1.2 Flutter 对鸿蒙的支持现状与方案取舍
我在动手之前先确认了 Flutter 对鸿蒙的支持程度。坦白说,Flutter 官方主线当时并没有直接生成鸿蒙工程的能力,但 OpenHarmony SIG 维护的 flutter_flutter 分支已经可以在鸿蒙真机上跑起来。这不意味着所有 Flutter 插件都能直接用,但至少核心框架、渲染引擎、Dart 代码执行这些部分是可以托底的。
Flutter 采用的是自绘渲染引擎,不依赖系统 WebView,也不依赖原生控件,因此在鸿蒙、Android、iOS 上看到的界面效果可以做到基本一致。这点对天气类应用非常重要,因为图表、卡片、动画这些 UI 动效,如果用 Web 技术做,三端表现很难统一;如果用原生做,等于要维护三套代码。Flutter 等于把“视觉一致性”这个问题直接消掉了。
但代价也很明显:部分原生插件在鸿蒙上没有现成实现,比如定位、通知这类能力,可能需要自己通过 MethodChannel 桥接到鸿蒙原生侧。做技术选型不能只看优点,要把这部分工作量也算进去。
1.3 为什么不是 ArkUI 原生,也不是 Tauri
如果只做鸿蒙一个平台,我大概率会用 ArkUI 原生开发,毕竟系统级能力调用最直接,性能也最好。但我的场景是跨平台,已经有一份 Flutter 业务逻辑要复用,再单独维护一套 ArkTS 原生版本,后续迭代成本会翻倍。
Tauri 我最初也看过。Tauri 包体确实小,但它在移动端依赖系统 WebView,不同平台的渲染差异比较明显;更麻烦的是定位、后台通知、进程保活这些能力,在鸿蒙的 WebView 约束下做起来很吃力。花粉浓度查询看起来简单,实际上对“定位准确性”和“通知触达率”要求都不低,Web 方案在这种场景下的体验并不稳定。
所以最终选择是:Flutter 作为跨平台框架,鸿蒙作为重点适配目标,Android/iOS 作为附带输出。这个选型不是因为它最时髦,而是它最适合“一个人同时维护几个平台”的独立开发者状态。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 花粉浓度数据的获取链路:公开接口、数据清洗与缓存策略
2.1 数据源怎么找、怎么判断能不能用
花粉浓度数据不像天气数据那么普及,我花了不少时间在数据源上。目前可用的来源大致分三类:
- 气象部门发布的过敏指数或花粉指数,部分会通过官网或开放平台提供,更新频率和稳定性差异较大。
- 商用天气服务商,比如心知天气、和风天气这类服务,会有“过敏指数”或“花粉指数”字段。申请 Key 后调用,有免费额度,适合个人项目。
- 学术机构或本地花粉监测站的公开数据,字段往往很专业,但更新频率通常较低,不太适合做“实时查询”。
判断一个数据源能不能用,我给自己定了几条标准:更新频率最好在一小时以内;返回字段要稳定,不能今天叫 pollen、明天叫 pollenIndex;授权条款要清楚,商用和个人项目的边界要明确。我的做法是,以商用天气 API 为主数据源,再拿公开页面数据做交叉校验。多源对比的好处是,当某个源数据明显偏离历史区间时,系统能自动标记异常。
2.2 服务端抓取与归一化:从多源数据到统一模型
这里有一个很多人容易忽略的问题:不要直接让手机端去请求天气服务商的接口。用户一多,频率限制、费用、稳定性都会出问题。所以我加了一层非常轻量的后端服务,用定时任务每 30 分钟抓一次数据,清洗后写入 Redis,再对外提供一个只读 API 给 Flutter 客户端。
清洗看起来简单,做起来全是细节。不同数据源的单位不一样,有的是“浓度指数”,有的是“粒/千平方毫米”,有的只给“低中高”等级。聚合时还要按城市和区域合并,用可信度做权重。比如某个城市商用 API 数据齐全,另一个城市只有气象站数据,我宁愿返回“暂无数据”也不硬拼一个错误结果。
给客户端返回的数据结构我简化成下面这样:
json复制{
"city": "北京",
"location": { "lat": 39.9, "lng": 116.4 },
"pollen": {
"level": 3,
"level_text": "偏高",
"value": 121,
"unit": "粒/千平方毫米",
"species": [
{ "type": "树木", "value": 68 },
{ "type": "杂草", "value": 41 },
{ "type": "禾草", "value": 12 }
]
},
"updated_at": "2025-04-08T08:00:00+08:00"
}
这里 updated_at 必须带上。客户端要展示“数据更新时间”,否则“实时查询”四个字没有说服力。用户看到 10 分钟前的数据,比看到一条没有时间戳的旧数据放心得多。
2.3 客户端缓存与“实时”的度
花粉浓度不是股票行情,它不会一秒一变。我最后把客户端的刷新策略定为:App 回到前台拉一次,驻留后台时每隔 60 分钟尝试刷新一次。同时把上一次成功拿到的数据用 shared_preferences 落到本地。这样在电梯、地库这些弱网场景下,打开 App 先渲染缓存,网络数据回来后再更新 UI,不会出现一整页空白。
我见过不少天气类应用,明明数据半小时没更新,也要转半天菊花。做实时查询的 App,首先要定义清楚“实时”到底是什么意思:对花粉浓度这种指标来说,30 分钟内的数据已经足够实时。频繁请求只会增加电量和流量消耗,反而让用户觉得卡。
3. 鸿蒙端 Flutter 工程搭建:HAP 构建、权限声明与坑点
3.1 工程生成与工具链:VSCode 和 DevEco 怎么配合
我在搭建环境时踩了第一个大坑:直接创建普通 Flutter 工程,发现根本没有 ohos 目录。原因是官方命令默认不会生成鸿蒙平台工程,需要使用支持 ohos 平台的 Flutter SDK 分支来创建。这个分支在 OpenHarmony SIG 的仓库里维护,配置好之后再执行项目创建,才会多出一个 ohos/ 目录。
很多人问 VSCode 能不能开发 Flutter 鸿蒙应用。我的答案是:Dart 部分在 VSCode 里写完全没问题,但最终构建 HAP、调试原生侧代码,还是需要用 DevEco Studio 打开 ohos/ 目录,通过 hvigor 工具链构建。所以我的开发流程是:日常用 VSCode 写 Dart 和测试业务逻辑,真正打包上真机时切到 DevEco Studio。注意不要把 DevEco 的项目根目录选成整个 Flutter 工程,要选 ohos 子目录,否则会构建失败。
设备连接方面,鸿蒙真机开启开发者模式后,使用 hdc 工具就能看到设备列表。第一次连接时如果提示设备未授权,记得在手机上确认调试授权,这跟 Android 的 adb 类似。
3.2 网络、定位、通知权限声明
鸿蒙的权限模型比 Android 更严格。除了要在代码里动态申请,还必须先在 ohos/entry/src/main/module.json5 里声明使用权限。我实际用到的权限包括:网络访问、精确定位、模糊定位以及通知权限。定位权限尤其要注意,鸿蒙把精确和模糊定位分开了,应用如果只申请精确定位而不申请模糊定位,在某些设备上可能反而拿不到结果。
json5复制{
module: {
requestPermissions: [
{ name: "ohos.permission.INTERNET" },
{ name: "ohos.permission.LOCATION" },
{ name: "ohos.permission.APPROXIMATELY_LOCATION" }
]
}
}
需要提醒一点:权限名称必须以你使用的鸿蒙 SDK 版本官方文档为准,不同版本可能略有差异。如果在 module.json5 里写了不存在的权限,构建过程不一定报错,但安装后会被系统悄悄忽略,上架审核也可能被问到。我在开发早期就吃过这个亏,定位权限一直拿不到,排查很久才发现是权限名称写错了。
3.3 定位模块在鸿蒙上的行为差异
Flutter 生态里常用的 geolocator 插件,在鸿蒙上不一定有官方实现。我试过直接引入,结果调用后一直没有回调,也没有报错,非常诡异。最终的解决思路是绕开插件,用 MethodChannel 自己写一个通道,在鸿蒙原生侧调用系统的定位能力。
Flutter 侧代码大致是这样的:
dart复制const platform = MethodChannel('pollen_guard/location');
Future<Map<String, double>> getCurrentLocation() async {
final result = await platform.invokeMethod('getCurrentLocation');
return {
'lat': result['lat'] as double,
'lng': result['lng'] as double,
};
}
鸿蒙原生侧在 ets 文件里注册对应的 Channel,调用系统定位接口。这里有个经验:原生侧的定位结果是异步回调,Flutter 侧不要一直 await 不设超时。真机上定位有时要好几秒,我加了一个 8 秒超时,超时后直接提示用户手动选择城市,而不是让页面一直转圈。
权限被拒绝的场景也必须处理。我的方案是:定位失败或拒绝时,首页显示城市选择器,用户可以手动输入或选择常驻城市。一个健康类应用,不能让用户因为拒绝定位权限就完全无法使用。
4. 实时查询与过敏提醒的核心逻辑:阈值模型、状态管理与通知
4.1 花粉浓度分级与防护建议
花粉浓度没有一个全球统一的标准,不同机构的分级差异很大。所以我在后端做归一化时,就已经把等级算好,客户端只负责把等级映射到 UI 和防护建议。这是我项目内部使用的映射表,不一定通用,但思路可以参考:
| 等级 | 浓度参考范围 | 防护建议 |
|---|---|---|
| 1 低 | 0~50 | 正常活动 |
| 2 中 | 51~100 | 敏感人群适当防护 |
| 3 偏高 | 101~200 | 建议减少长时间户外活动,戴口罩 |
| 4 很高 | 201~400 | 关闭门窗,外出必须防护 |
| 5 极高 | 大于 400 | 尽量避免外出,开启空气净化器 |
这张表的好处是,不管数据源给的是“指数”还是“粒/千平方毫米”,最后都能统一成用户能理解的等级。给用户做提醒时,只说“偏高”是不够的,一定要跟上“怎么做”。比如“建议佩戴口罩”就比“过敏指数较高”有价值得多。
4.2 状态管理:Riverpod 在项目里的用法
状态管理我用了 flutter_riverpod。选择它不是因为功能最多,而是因为 FutureProvider 和 autoDispose 非常适合处理“异步数据 + 页面销毁自动取消”的场景。花粉浓度数据天然是异步加载,用 Riverpod 可以很干净地把加载、成功、错误三种状态拆开。
核心代码大致是这样:
dart复制final locationProvider = FutureProvider<LocationResult>((ref) async {
return LocationService.getCurrent();
});
final pollenDataProvider = FutureProvider.autoDispose<PollenData>((ref) async {
final loc = await ref.watch(locationProvider.future);
return PollenRepository().fetch(loc.lat, loc.lng);
});
UI 侧使用 ref.watch(pollenDataProvider).when(...) 来处理 loading、data、error 三种状态。autoDispose 保证了页面销毁后,Provider 内部的异步任务会被自动取消,不会出现数据回来之后还去更新一个已经不存在的界面。
这里有个小细节:不要把整个 LocationService.getCurrent() 塞进 pollenDataProvider 里。定位和网络拉取分开两个 Provider,可以单独测试,也可以在需要时手动刷新其中一个。
4.3 阈值模型、通知渠道与用户偏好
“智能过敏防护”的核心不只在于浓度数据准,更在于提醒方式对不对。我给用户提供了三个设置项:过敏原类型、触发提醒的等级阈值、可接收提醒的时间段。用户如果只对树粉过敏,那么即便杂草浓度很高,也不应该频繁打扰他;用户把阈值设为“偏高”,那么“中”等级就不推送。
提醒策略上,我用了本地通知每天早晨 7:30 推送一次今日花粉预告,同时当等级超过用户阈值时触发一次即时提醒。为了不让用户被重复轰炸,客户端会记录“今天已经提醒过某个等级”,等级没变就不重复推送。
后台任务在鸿蒙上有个现实问题:如果 App 进程被系统清理,本地通知定时器可能失效。更稳妥的做法是服务端每天定时调用推送通道,客户端收到推送后解析内容并展示。这块设计要提前想清楚,否则用户会抱怨“App 装了但从来不提醒”。
5. 可视化与交互设计:怎么让用户一眼看懂该不该防护
5.1 首页的信息层级:结论优先,数据次之
做天气类应用最容易犯的错,就是打开首页先看到一堆数字和图表,用户却不知道今天到底该不该戴口罩。花粉浓度查询的用户,打开 App 的第一诉求是“决策”,不是“看报表”。所以我首页第一眼是“今日花粉浓度:偏高”这个大结论,下面跟着一句防护建议“敏感人群建议佩戴口罩”。再往下才是具体数值、主要过敏原和趋势图。
颜色上用绿色到红色渐变表达等级,但同时用图形和文字辅助说明,避免只靠颜色区分。很多时候用户早上刚醒,眼神都还没聚焦,一个“低浓度”的绿色圆圈比一段描述有用的多。
5.2 趋势图与每日峰值曲线
趋势图我用 fl_chart 绘制最近 24 小时浓度曲线和未来 7 天趋势柱状图。刚开始我把数据点全部画上去,低端鸿蒙真机出现明显掉帧。后来把曲线数据点控制在 24 个左右,并把图表用 RepaintBoundary 包起来,减少无关区域的重绘,问题就解决了。
每日峰值时段对过敏人群特别有用。花粉浓度通常不会一整天空着,而是有上升、峰值、下降的过程。我在这张图上标出了“当前时刻”所在位置,并用文字提示“预计今晚 6 点浓度达到峰值,建议提前回家”。这种信息比单纯展示曲线更有用。
5.3 个性化配置与防护建议
设置页不要做成简单的列表,要围绕“过敏防护”来做引导。我会让用户选择自己过敏的类型:树木、禾草、杂草,然后根据当天的 species 信息判断“今天的主要过敏原是不是你敏感的那类”。如果用户选了树木过敏,而今天浓度最高的正好是树粉,防护建议就会自动加粗提示。
防护建议是我根据浓度、过敏原类型、未来降水概率综合生成的规则,比如“降雨后浓度可能下降,可以适当安排通风”。所有建议都只是生活参考,不是医学诊断,页脚要留一句说明,避免误导用户。
6. 真机调优记录:鸿蒙上的性能、包体积与常见崩溃
6.1 冷启动与首帧优化
在鸿蒙真机上,最初从点击图标到出现第一个 Flutter 页面,体感大概两秒多。排查后发现启动阶段初始化了图表库、定位服务、通知插件,这些其实都不是首帧必需的东西。优化思路是:首帧只加载本地缓存数据和最简单的页面骨架,图表库等页面可见后再初始化,定位请求也延迟到页面出现后再发起。
同时,我把调试日志用 --dart-define=ENV=prod 关掉,避免 release 包还在打印大量日志。优化之后,冷启动体感压到 1.3 秒左右。这个数据不算极致,但对一个查询工具类 App 已经够用。
6.2 HAP 体积控制
Flutter 应用打出来的 HAP 包天然比纯 ArkUI 应用大,因为要带一套自绘引擎。我第一次打出来的 HAP 超过 40MB,把我吓了一跳。后来做了几件事:移除用不到的插件、把启动图和页面图片转成 WebP、开启 AOT 编译和混淆。最终 HAP 降到 30MB 左右。30MB 对现在的应用来说不算大,但相比纯鸿蒙原生应用确实还是胖一圈。如果未来把地图、语音这些重型能力加进来,包体积还会涨,到时候需要再考虑按需加载。
6.3 常见崩溃与内存泄漏
我遇到最典型的崩溃来自两处:一是 Timer.periodic 忘记取消,App 进入后台后定时器还在跑,定位服务一直被 hold 住,时间长了被系统杀掉;二是异步回调回来时页面已经销毁,还在用 setState 更新 UI,导致崩溃。
解决办法其实很常规,但很多人会漏:所有定时器在 dispose 里统一取消;异步服务使用 Riverpod 的 autoDispose,让 provider 生命周期跟着页面走;每次网络请求返回后,先判断当前页面是否还在。用 DevEco Studio 自带的 profiler 可以很直观地看到线程活跃度和对象引用情况,排查这类问题比靠猜快得多。
7. 工程化、上架与后续演进
7.1 多端构建、CI 与版本管理
Flutter 项目同时存在 android、ios、ohos 三个目录后,版本管理要特别小心。ohos 目录不能每次用工具链重新生成,否则手动配置过的权限声明会被覆盖。我自己的做法是:ohos 目录纳入版本库,Dart 业务代码和原生侧代码一起提交,但原生侧尽量少做逻辑,只保留通道桥接和权限配置。
CI 这里有个现实问题:Flutter 常规的构建命令只处理 Android 和 iOS,鸿蒙 HAP 需要走 DevEco 的命令行工具。我在团队内部是先构建 Android/iOS 包走常规 CI,鸿蒙包在有 DevEco 环境的打包机上单独构建。如果你的项目是纯个人开发,手动构建 HAP 也完全够用,不用一上来就上完整流水线。
上架鸿蒙应用市场时,隐私政策、权限说明、数据来源授权这三样一定要提前准备。花粉浓度数据如果来自商业服务商,要确认你的套餐是否允许再分发;如果来自公开气象数据,最好保留页面截图和抓取时间记录,方便审核时解释。
7.2 后续扩展:花粉预测、服务卡片与智能家居联动
现在这个版本已经能解决我的日常需求,但后续可做的方向还有不少。比如花粉预测,可以结合温度、湿度、风速、季节和植被日历,做一个轻量回归模型,预测未来 24 小时浓度趋势。不过这类预测不能直接照搬气象模型,不然误差会大到被用户骂。
桌面卡片方面,鸿蒙的服务卡片是原生 ArkTS 能力,Flutter 并不能直接生成。可行的思路是做一个原生 2x2 卡片展示今日等级和一句建议,点击卡片后跳回 Flutter 主页面。这样用户不用打开 App 也能一眼看到“今天要不要防护”。
智能家居联动也很有意思。如果家里的新风机、空气净化器有开放接口,可以在浓度过高时自动切换模式。这个功能本质上就是“花粉浓度查询”的价值延伸——从“提示用户”变成“替用户做点事”。
我在这个项目里最大的体会是:实时查询不要做成高频轮询,真正重要的是把浓度数据翻译成用户能直接行动的结论。Flutter 框架跨平台鸿蒙开发这条路目前还谈不上完美,但跑通之后,一套代码覆盖三端的收益确实明显。如果你也正在做类似的健康或天气类应用,建议先把数据源和提醒逻辑想清楚,再回头调 UI 和动画,这样项目会顺很多。
