1. 为什么这个时候谈 weather_pack 的鸿蒙化适配上个月我把公司一个生活服务 App 的主要模块迁到鸿蒙端里面最有代表性的一块就是天气能力当时用的是 Flutter 生态里的 weather_pack。普通跑在 Android 和 iOS 上都挺顺一换到鸿蒙工程直接报 MissingPluginException天气数据、定位、缓存全部趴窝。这篇文章不是口嗨是我们把 weather_pack 从依赖边界、MethodChannel、Provider 状态层、缓存再到鸿蒙卡片完整迁到 OpenHarmony 容器里的过程记录也把全场景气象数据中心怎么抽出来的思路写出来给所有准备做鸿蒙端 Flutter 适配的同学一个可以照着抄的清单。1.1 天气数据在生活服务应用里的位置生活服务类 App 里的天气模块表面看只是首页一个卡片实际上它是整个信息架构的“时间锚点”。用户打开 App 的第一眼往往不是商品推荐而是“今天要不要带伞”“明天要不要洗车”。天气模块做得好不好直接决定用户愿不愿意把 App 留在桌面上。天气数据链路通常不复杂但很琐碎UI 展示温度、湿度、风力、空气质量背后要接实时天气、逐小时预报、未来七天趋势、生活指数、天气预警每类数据更新频率和安全级别都不一样。weather_pack 这种三方库的价值就是把这些琐碎收敛成一棵相对清晰的数据树。它对外暴露的通常是几个模型类加一个加载入口比如 CurrentWeather、DailyForecast、AirQuality、WeatherConditionType底层是 HTTP 拉数据、JSON 解析、字段映射、天气图标匹配。大部分能力在 Dart 层就能完成这也是它能跨端的根本原因。真正麻烦的是定位、网络权限、系统时区、字体渲染这些“需要原生系统表态”的能力到了鸿蒙上必须单独接一遍。1.2 鸿蒙端 Flutter 生态的现状现在做鸿蒙适配有一个基本事实鸿蒙上的 Flutter 已经不是“能不能跑”的问题而是“插件生态有没有跟上”的问题。OpenHarmony 的 Flutter 引擎由社区持续维护Dart 层几乎可以原样搬过去但原生插件要重新实现依赖关系要从 Java/Kotlin 换到 ArkTS 或 C构建产物从 APK/AAB 变成 HAP包管理也从 Gradle 换到 hvigor。这个阶段最容易踩的坑就是大家误以为“只要 Flutter 能跑三方库就能直接跑”。实际上 Flutter 框架只是把 UI 和 Dart 层运行时搬过去了插件背后的 MethodChannel 必须有人接。weather_pack 这类组件如果本身带定位依赖到了鸿蒙上默认就是空的你调什么都会返回 “Not implemented”随后冒出来的 MissingPluginException 其实是一个信号该去查插件的原生注册逻辑了。2. 动手前先拆清楚 weather_pack 的依赖边界很多人拿到适配需求第一反应是“把源码拉过来改”这在大项目里是大忌。鸿蒙适配的难点不在改代码而在“知道哪些代码需要改”。所以第一步不是写代码是拆依赖。2.1 区分 Dart 侧能力和原生侧能力weather_pack 这种天气库通常可以分成三层第一层是纯 Dart 数据模型层负责把 JSON 映射成 CurrentWeather、DailyForecast、AirQuality 这些对象。只要不是用了 dart:io 里只在 VM 上存在的能力这层几乎不用动。第二层是数据获取层会用 http 或者 dio 请求天气 API。这一层依赖的是网络能力而网络能力在鸿蒙端基本是通的需要额外注意的是 TLS 版本、域名备案、IPv6 环境、请求超时这类问题。第三层是设备能力层主要是定位。很多天气库的默认逻辑是“先拿定位经纬度再请求天气接口”。定位在 Flutter 里往往不是自己写的而是通过 geolocator、location 这类插件去调系统定位服务。到了鸿蒙上这些插件如果没有鸿蒙实现那 weather_pack 的整套链路就会断在最前面。你可以这样理解Dart 层是精装修原生层是水电煤。精装修可以跨城市复用但水电煤得重新接。2.2 五步清单用命令把依赖边界摸到底建议按这个顺序做一次依赖体检不要上来就全局搜索“鸿蒙”关键词第一步打开 pubspec.yaml看 weather_pack 直接依赖了哪些包。重点看有没有 location、geolocator、path_provider、shared_preferences、connectivity_plus 这类带原生实现的三方库。第二步在项目根目录跑flutter pub deps --stylecompact它会展开整个依赖树比看 pubspec 更直观。你需要关注的是间接依赖有时候 weather_pack 自己没直接依赖定位但它依赖的另一个包默默带了原生插件这种最容易漏。第三步去.dart_tool/package_config.json里确认实际解析到的包版本防止本地缓存和线上依赖不一致。第四步把定位、本地存储、网络状态这三类插件单独挑出来逐个判断有没有鸿蒙实现。判断方法很简单去 pub.dev 或者对应仓库看是否声明了ohosplatform或者是否存在ohos目录。第五步建立一个“原生依赖缺口表”例如 weather_pack 需要定位但 geolocator 当前没有鸿蒙插件支持那就要在桥接层自己实现一个极简定位通道。我在实际项目里遇到过一种情况weather_pack 的依赖树里带了一个老版本的 shared_preferences这个老版本在鸿蒙上注册失败导致整个 Flutter 容器启动后 MethodChannel 全部异常。问题本身和 weather_pack 没关系但报错全在天气模块里冒出来排查起来特别迷惑。所以依赖树一定要在动手前拉清楚。2.3 适配策略能不碰的业务逻辑尽量不碰依赖边界摸清楚之后要定一条铁律天气库内部的业务逻辑尽量不要改所有适配动作都收口在“外围”。这句话的意思是不要为了适配鸿蒙去改 weather_pack 的默认行为比如不要改它的缓存策略、不要改它默认的 API 参数拼接规则。你应该做的是给这个库提供一个适配好的“平台实现”让它调用定位时能拿到鸿蒙系统的坐标让它缓存时能写到鸿蒙沙箱目录让它请求网络时能走鸿蒙的网络栈。在工程上这对应“依赖注入”和“适配器模式”。哪怕 weather_pack 没有给你留接口你也可以在自己的业务层外面包一层 Repository把 weather_pack 当作数据源之一封装在内部由你的 Repository 决定“先拿定位还是先读缓存”。这样以后 weather_pack 升级、鸿蒙引擎升级你只需要修改适配器不需要动页面代码。我当时还额外做了一件事把 weather_pack 的入口封装成了WeatherRepository接口内部再分别实现AndroidWeatherRepository、iOSWeatherRepository、OhosWeatherRepository。页面层只依赖 Repository 接口完全不感知底层跑的是哪个平台。这看起来多了一点代码量但后续调试和降级回滚非常省事。3. 鸿蒙工程准备从 Flutter 工程到 ohos 平台依赖边界拆完接下来就是把工程真正转成“能在鸿蒙上构建”的状态。这一块不用改业务代码但环境配置一步错后面全白干。3.1 把鸿蒙 SDK 和 Flutter 引擎装到本机鸿蒙上的 Flutter 开发本质上还是用 Flutter 工具链但需要切换到支持 OpenHarmony 的 SDK 分支。社区习惯叫flutter_flutter也就是可以同时管理标准 Flutter 和鸿蒙 Flutter 的那套环境。安装完成后你的本机会有两套 Flutter SDK一套是官方分支一套是鸿蒙分支。日常写代码、跑测试用官方分支没有任何问题。只有在编译hap包、调试鸿蒙原生插件时才需要把PATH切到鸿蒙分支。我个人的做法是写两个切换脚本比如use_ohos.sh和use_standard.sh避免来回手改环境变量。这个看起来是个小节但能帮你省下大量“刚才还能编译现在怎么报 SDK 版本不对”的困惑。鸿蒙 SDK 本身也要在 DevEco 或者命令行环境里装好要求 Node.js、hvigor、OpenHarmony SDK 的版本能对得上。这里有个不容易察觉的问题鸿蒙 Flutter 分支的版本迭代速度比标准 Flutter 慢你本地 OpenHarmony SDK 如果太新或者太旧编译hap时会出现引擎版本和平台版本不匹配。遇到类似问题先别急着改代码检查版本组合往往是最快的解法。3.2 给现有 Flutter 工程补上 ohos 平台目录老工程没有 ohos 目录时不用手搓整个目录结构。比较稳妥的方法是用脚手架生成一个全新的鸿蒙 Flutter 工程然后把你的lib/、assets/、pubspec.yaml迁移进去。具体步骤是先在工作区里用支持鸿蒙的 Flutter 分支跑flutter create --platforms ohos .它会生成ohos目录以及鸿蒙工程必需的文件比如ohos/hvigorfile.ts、module.json5、entry/src/main/ets/entryability/EntryAbility.ets等。然后再把自己的业务代码拷进来再把 pubspec 依赖重新拉一遍。这一步不要偷懒直接复制别的项目的 ohos 目录因为包名、应用标识、签名信息都不同。用脚手架生成之后再改名字比“从零手写”要安全得多。生成后记得检查ohos/AppScope/app.json5里的bundleName以及module.json5里的package_name这两个值会直接影响最终 HAP 的安装和互相覆盖。3.3 配置权限声明和目标设备天气类应用在鸿蒙上需要的权限一般包括定位权限、网络权限、可能还有通知权限用于天气预警。这些权限不是在 Flutter 的 AndroidManifest 里声明而是在鸿蒙的module.json5里通过requestPermissions字段声明。例如定位权限通常对应{ name: ohos.permission.LOCATION }网络权限一般对应{ name: ohos.permission.INTERNET }如果你的应用还要在后台定时刷新天气需要额外关注“后台任务”和“数据采集”限制前期适配可以先用“前台获取定位 进入前台刷新”的方式降低复杂度。另外鸿蒙对定位权限分精细定位和粗略定位天气这种场景用粗略定位基本就够既能减少权限弹窗的压迫感也能降低被系统拒绝的概率。权限配置看似简单但很多人忽略了一点修改module.json5后必须重新生成 HAP不能只靠热重载去验证权限。权限是系统层面的能力Flutter 热重载不会重新加载module.json5。4. 核心实操MethodChannel 桥接、定位、网络与天气图标工程骨架搭好后进入最核心的适配环节。这一节的内容本质上是把 weather_pack 曾经依赖的 Android/iOS 原生能力在鸿蒙侧重新实现一遍。4.1 用 MethodChannel 把定位能力接到 ArkTS如果 weather_pack 内置的定位插件在鸿蒙上不可用最直接的方案是自己写一个极简定位通道。Dart 侧可以这样定义一个通道class OhosWeatherPlatform { static const MethodChannel _channel MethodChannel( weather_pack/location, ); static FutureMapString, double getCoordinate() async { try { return await _channel.invokeMethod(getCoordinate); } on PlatformException catch (e) { return String, double{latitude: 0, longitude: 0}; } } }在鸿蒙的 ArkTS 侧需要在一个 ability 中注册这个 MethodChannel并在里面实现getCoordinate方法。整体结构类似import { MethodChannel } from ohos/flutter_ohos; const channel new MethodChannel(weather_pack/location); channel.setMethodCallHandler((call) { if (call.method getCoordinate) { // 调用 ohos.geoLocationManager 获取定位 // 返回经纬度 Map } });定位是这套链路里最容易出问题的点。鸿蒙的定位框架强调“先申请权限、再确认开关、再回调结果”不像 Android 老版本那样调一下系统 API 就能拿到结果。所以桥接层里一定要处理好两个回调状态用户拒绝授权、定位超时。我当时在桥接层里加了一个 8 秒超时保护超时后返回默认坐标并提示 “Use city fallback”页面会退回手工选择城市模式。4.2 网络与 API Key 的配置思路weather_pack 通常支持通过配置类传入天气数据的 API Key。注意不要把这个 Key 写死在代码里在鸿蒙工程中也要避免把 Key 写进可以被反编译的资源文件。建议放到entry层的运行时环境变量里或者通过 NativeConfig 传递到 Dart 层。网络层在鸿蒙上基本不需要改 Dart 代码但有一个细节值得注意天气 API 的域名必须支持 IPv6因为鸿蒙设备在部分网络环境下会优先走 IPv6如果不支持会出现“Android 上天气正常、鸿蒙上一直转圈”的现象。另外TLS 版本建议在鸿蒙网络栈上做一次握手验证有的老域名只支持 TLS 1.1在鸿蒙默认安全策略下握手会失败。一个稳妥的验证方法是在鸿蒙设备上先用自带的浏览器打开天气 API 的普通请求链接确认可以快速返回 JSON如果浏览器能打开但 App 里拉不下来优先查证书链和时钟同步问题其次是请求头里缺了 User-Agent。4.3 天气图标字体在鸿蒙上的渲染问题天气图标是一个很隐蔽的坑。weather_pack 通常附带一套图标字体字体文件会被打包到 assets 里。理论上 Flutter 的字体渲染是自绘的与系统字体无关所以图标应该没问题。但现实是鸿蒙 Flutter 容器在部分版本上对自定义字体的解析有兼容问题尤其是字体文件里包含大量私有 Unicode 码位时可能显示为豆腐块。处理方案有两个。优先把 weather_pack 的天气图标从字体映射改成普通的图片资源代价是包体会变大但渲染最稳定。如果不想改图就要检查字体文件的校验确认 pubspec 中正确的 fontFamily 写法和 asset 路径。另外鸿蒙系统对字体文件的版权校验比 Android 严格。网上随便下载的 ttf 字体可能因字体表结构不规范导致解析失败建议优先使用 weather_pack 自带的字体不要额外替换。5. Provider 状态管理让天气数据在页面间串起来weather_pack 本身不管状态管理但一个完整的天气模块必须有状态管理。我们的项目里用的是 Provider这是 Flutter 社区里最简单、最容易讲清楚的一种方案在鸿蒙适配中也没有任何额外成本因为它是纯 Dart 层能力。5.1 为什么用 Provider 而不是全局单例天气数据有以下特点多个页面会同时读取首页、详情页、城市管理页、卡片数据有固定刷新周期需要区分“正在加载”、“加载成功”、“加载失败”这三种状态。如果只是用一个全局单例保存数据页面间可以读到数据但页面无法感知数据刷新完成也就无法自动更新 UI。Provider 的ChangeNotifier解决的就是这个问题。你把天气数据放进一个WeatherViewModel页面通过context.watchWeatherViewModel()去监听。数据刷新时模型notifyListeners()所有正在监听 UI 会同步重建。这个机制在鸿蒙端和 Android/iOS 上没有任何区别。5.2 一个最小可用的天气 Provider 实现下面是一个很基础但不简陋的写法核心是把状态和业务逻辑分开class WeatherViewModel extends ChangeNotifier { WeatherViewModel(WeatherRepository repository) : _repository repository; final WeatherRepository _repository; WeatherState _state WeatherState.initial(); WeatherState get state _state; Futurevoid refresh() async { _state _state.copyWith(loading: true, error: null); notifyListeners(); try { final current await _repository.fetchCurrentWeather(); final forecast await _repository.fetchDailyForecast(); _state _state.copyWith( loading: false, current: current, forecast: forecast, ); } catch (e) { _state _state.copyWith( loading: false, error: e.toString(), ); } finally { notifyListeners(); } } }把 Model、Repository、ViewModel 分开之后天气模块的鸿蒙适配就变得非常舒服。你在桥接层里修定位、修缓存页面层的代码几乎一行都不用动。5.3 刷新、缓存与错误态放进同一个状态机天气模块的刷新策略并不是“每次进入页面都拉一次”那样不仅费流量还会频繁触发定位权限弹窗。合理的做法是在 ViewModel 里维护一个状态机首次加载走骨架屏数据新鲜度小于 30 分钟直接用缓存超过 30 分钟重新拉取拉取失败时保留旧数据并用一个静默提示让用户知道“这是上次的数据”。状态机的好处是用户感知非常平滑。就算是完全断网App 里依然能看到上一次的天气数据而不是一张加载失败的空页面。对生活服务类应用来说“显示旧数据”永远比“显示错误页”更符合用户心理预期。这个状态机的代码不要分散在各个页面里收口在WeatherViewModel内部即可。鸿蒙卡片要读取数据时也是通过同一个 Repository 和状态机不单独走一套逻辑。6. 全场景气象数据中心本地缓存、卡片与多端同步适配做完后我并没有停留在“能在鸿蒙上跑起来”这个阶段而是把天气模块顺势重构成了一个“全场景气象数据中心”。核心思路是把天气数据从“页面级临时变量”提升为“设备级共享资源”让应用页面、桌面卡片、元服务卡片读取同一份可信数据。6.1 数据层设计的两个核心单一返回源、统一时间戳所谓数据中心首先要定“单一返回源”。所有天气数据的读取都走同一个入口例如WeatherRepository.getWeather(City city)不允许每个页面各自直接去调 weather_pack 的 API。只有这样缓存和更新策略才能集中管理。第二个核心是“统一时间戳”。每个城市、每类天气数据都要记录updatedAt和expireAt。卡片是否需要刷新不靠“进页面就刷新”而是靠时间戳判断。否则多个入口各自刷新很容易出现 App 里显示 26 度、桌面上却显示 24 度这种“设备内部打架”的体验。时间戳的精度不需要到毫秒分钟级别足够。数据返回时统一校准到设备本地时区避免因为时区偏移导致卡片上出现“刚刚更新”但实际是很久以前的数据这种误判。6.2 卡片和元服务的展示策略鸿蒙的卡片机制非常强调“轻量、及时、不常驻”。卡片上展示天气时不要直接在卡片里跑 Flutter 渲染。正确做法是Flutter 负责在 App 内更新数据到本地数据库卡片侧通过系统能力读取同一份存储并渲染为原生卡片。这个策略的好处是卡片不依赖 Flutter 引擎的启动时间秒开。代价是需要额外维护一套卡片数据映射层把 weather_pack 的数据模型映射成卡片可读的简单键值结构比如temperature、condition_code、humidity、update_time。这一步在“元服务”场景下尤其重要。元服务周期短、启动轻量不能为了显示一个温度值就去初始化整个 Flutter 引擎。把天气数据中心化后元服务只读数据不跑业务逻辑启动速度飞快。6.3 从“拉一次”到“用一段时间”缓存更新策略我采用的缓存策略是三层内存缓存、本地数据库、远端接口。内存缓存负责页面秒开本地数据库负责卡片和冷启动读取远端接口负责真正刷新。具体参数可以这样设实时天气缓存 30 分钟5 分钟定位一次可接受但不建议频繁刷新。逐小时预报缓存 1 小时。未来七天预报缓存 6 小时因为这场数据一天变化有限。天气预警缓存 15 分钟预警时效性要求高。这个策略有一个隐藏前提天气数据必须带“城市标识”做分区。如果用户切换到另一个城市不能因为缓存了上一个城市的数据就直接展示。我用的是城市联动的缓存 Key例如weather:current:101010100这样即使多城市切换也不会串数据。7. 适配期最容易踩的 6 个坑和排查方法适配一周踩坑无数。这里把最有代表性的 6 个问题整理成小表格每个问题后面附上排查思路方便你对照检查。7.1 MissingPluginExceptionchannel 没有注册上这个异常是鸿蒙 Flutter 适配最常见的拦路虎。它代表 Dart 层调用 MethodChannel 时原生侧没有对应的 handler。排查顺序是先确认依赖包是否有ohos实现再确认插件是否在ohos工程中被显式注册最后确认 channel 名字是否和 Dart 侧完全一致。我在这个坑上花过两小时最后发现是 channel 名称少打了一个斜杠。MethodChannel 的名称匹配是纯字符串比较任何一个字符不一致都会静默失败。7.2 定位权限弹窗不出现 / 回调收不到鸿蒙的定位需要先声明ohos.permission.LOCATION还需要在应用中调用定位前检查授权状态。很多人只加了权限声明没有处理用户授权回调导致定位一直被拒绝。另外鸿蒙的定位回调是异步的如果你在 Flutter 侧用同步方式等待结果大概率会超时。我建议把定位封装成Future并做超时管理。7.3 天气图标变成豆腐块豆腐块是字体解析失败的典型表现。不要怀疑是 weather_pack 的 bug先确认字体文件有没有被正确打进 HAP。可以在鸿蒙侧打开 HAP 的 assets 目录看字体文件的大小是否和原始文件一致。如果文件缺失检查 pubspec 里的 asset 路径以及脚手架生成 ohos 目录时有没有把 assets 同步过去。7.4 热重载失效与 RCE 调试鸿蒙 Flutter 的热重载并不像 Android 那样完全可靠尤其是在修改原生层代码、修改module.json5后热重载不会生效。遇到“改了没反应”不要反复点 reload直接重新构建 HAP。调试时多用日志。Dart 侧用debugPrintArkTS 侧用hilog两边日志时间戳对齐通过时间轴判断卡在哪一层。这个方法看起来笨但在鸿蒙端调试特别有效。7.5 TLS 握手失败天气 API 在鸿蒙上拉不下来但页面和定位都正常优先排查 TLS。鸿蒙的网络安全默认策略更严格部分老旧证书链会被拒绝。解决方向是让运维把证书链补全而不是在代码里关掉证书校验。顺带说一句不要为了调试方便在鸿蒙工程里全局关闭安全校验那样既是安全隐患也过不了应用市场上架审核。7.6 多设备卡片不同步同一个账号登录手机和折叠屏卡片上的天气数据可能不一样。这是因为每个设备各自维护了本地缓存没有同步时间基线。我们的解法是在 Repository 层增加一个 “sync token” 概念每次刷新成功后会把时间戳写到本地卡片读取时如果发现 time bucket 不一致会拉取一次远端时间基线校准。8. 我在这次适配里学会的一件事适配做完我最深的体感是鸿蒙适配并不是把 Flutter 代码重写一遍而是把“平台边界”重新画一遍。weather_pack 的 Dart 层几乎没动但我们把定位、权限、缓存、卡片、图标、TLS 全部在鸿蒙侧重新接了一遍相当于给这套天气能力换了一个全新的“底盘”。如果你现在正好在鸿蒙端适配一个三方库我的建议是先建一个最小 Demo 工程——只放一个 MethodChannel、一张空白页面先把“Dart 调 ArkTS 返回一个字符串”这条路走通再去迁移完整功能。这条路通了什么 MissingPluginException、channel 不注册、权限回调收不到都会变得非常好定位。正式迁移时尽量通过 Repository 收口数据源别让页面直接依赖三方库。这样后续 weather_pack 升级、鸿蒙引擎升级你的改动面永远是可控的。最后一个小经验不要迷信 “把包 note 改成 ohos 平台就能跑” 这种说法。鸿蒙适配的功夫百分之八十花在依赖边界清晰度和平台桥接层质量上。把这层做扎实后续无论是接天气、接地图、接推送都会顺手很多。