☰
鸿蒙化适配实战:weather_pack迁移与MethodChannel桥接全解析
2026/10/10 16:57:09 网站建设 项目流程

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 --style=compact,它会展开整个依赖树,比看 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 Future<Map<String, 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.watch<WeatherViewModel>()去监听。数据刷新时,模型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; Future<void> 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 MissingPluginException:channel 没有注册上

这个异常是鸿蒙 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 侧用debugPrint,ArkTS 侧用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 平台就能跑” 这种说法。鸿蒙适配的功夫,百分之八十花在依赖边界清晰度和平台桥接层质量上。把这层做扎实,后续无论是接天气、接地图、接推送,都会顺手很多。

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

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

立即咨询