Flutter IANA 时区插件 iana_time_zone 鸿蒙适配落地,基于 systemDateTime 同步接口获取时区
开发工具: 华为云码道
原始仓库: ds914048/iana_time_zone
本文配套仓库: oh-flutter/iana_time_zone
DevEco Studio:
26.0.0(26.0.0.851)
iana_time_zone 把“设备当前时区叫什么”这一件事做成了一行代码:await IanaTimeZone.getIanaTimeZone 返回 Asia/Shanghai 这样的 IANA 时区 ID。它没有订阅、没有事件流、没有权限,Dart 侧只有一个静态 getter,原生侧只有一个方法。也正因为接口极小,适配时真正要处理的问题不在功能,而在平台判断和版本门槛这类容易被忽略的地方。
本文以 iana_time_zone 0.0.1 为例,介绍源码准备、OHOS 工程配置、ArkTS 实现、example 真机运行,以及一个用系统设置做对照的验证方法。

插件目前支持 ohos 平台,源码位于 AtomGit 配套仓库。文中的代码以提交 7f93ee48bdfc47bcd32caaa1d0cf07aa95582710 为参考,对应 TAG 0.0.1-ohos-1.0.0-beta.1。

这个库的仓库名、Dart 包名、通道名恰好都写作 iana_time_zone,省掉了“仓库名不是包名”的换算;只有 Android 侧的包名是 com.smile.iana_time_zone,与 Dart 包名不同。
一、插件简介与适配目标
时区在移动端有两种表达方式:一种是“当前比 UTC 多几小时”,另一种是“IANA 时区数据库里的区域名”,例如 Asia/Shanghai、America/New_York、Australia/Adelaide。后者能表达夏令时规则和同一偏移下的不同地区,跨端同步、日志排查、按用户所在城市展示时间时都更可靠。
iana_time_zone 返回的就是 IANA 名称。上游两个平台的实现思路完全一致:
- Android:
TimeZone.getDefault().id; - iOS:
TimeZone.current.identifier。
两者都直接读系统时区设置,不做任何换算。因此 OHOS 侧的适配目标也很明确:找到一个能返回同样语义的系统接口,让应用侧拿到的字符串与另外两端一致。
与持续订阅型的插件相比,本库的结构简单得多:没有 EventChannel,没有状态码枚举,没有回调注册,也没有传感器权限。Dart 层发起一次 invokeMethod,ArkTS 侧读一次系统时区,把字符串回传就结束了。
但“简单”不等于“直接能跑”。上游 Dart 代码里有一层平台判断:
if (Platform.isAndroid || Platform.isIOS) {
final String timeZoneId = await _channel.invokeMethod('getIanaTimeZone');
return timeZoneId;
}
return null;
OHOS 不属于这两个平台,所以原样发布到鸿蒙上,插件永远返回 null,而且不会抛异常、不会报错——界面只会一直显示“当前平台不支持”。这是本次适配的第一个必改点,也是后面 FAQ 9.4 要专门讲的问题。
二、环境准备
环境搭建参考社区文档:Flutter OH 开发环境搭建,完成 Flutter OH SDK 安装、环境变量和 DevEco Studio 配置。
完成后,在宿主机终端执行以下命令,确认当前选中的是支持 OHOS 的 Flutter 工具链,并能发现目标设备:
flutter --version
flutter doctor -v
hdc list targets


工程使用的工具链和 SDK 配置如下:
| 项目 | 版本或配置 | 用途 |
|---|---|---|
| Flutter OHOS SDK | 3.47.7-ohos-1.0.0 | Flutter 编译与 OHOS 平台工具链 |
| Flutter 分支 | oh-3.47-dev | 对应的开发分支 |
| Dart SDK | 3.13.3 | Dart 语言与包管理环境 |
| DevEco Studio | 26.0.0(26.0.0.851) | 打开 example/ohos、签名与构建 |
| HarmonyOS 开发套件 | 26.0.0(API 26) | 开发套件版本及对应的 API 级别 |
targetSdkVersion | 26.0.0 | 应用面向的行为版本 |
compatibleSdkVersion | 6.1.1(24) | 当前工程声明的最低兼容版本 |
| 插件版本 | 0.0.1 | pubspec.yaml 中的包版本 |
| 原生语言 | ArkTS | HarmonyOS 插件实现 |
| 插件产物 | HAR | 被应用 entry 模块依赖 |
2.1 开发套件版本与工程中的 SDK 版本配置
26.0.0(API 26) 和 26.0.0 涉及 HarmonyOS 开发套件版本(API 版本)及其底座 OpenHarmony 的版本号体系。在本文工程中,相关版本的含义与配置方式如下:
26.0.0(API 26)表示 HarmonyOS 开发套件版本为26.0.0,对应 API 26。26.0.0是本文 HarmonyOS 应用工程中targetSdkVersion的属性值,compileSdkVersion未显式声明,随 DevEco Studio 26.0.0 自带的 API 26 SDK 走默认值。6.1.1(24)是本文工程中compatibleSdkVersion的属性值,声明最低兼容 API 24。
对应的 product 配置为:
{
"name": "default",
"compatibleSdkVersion": "6.1.1(24)",
"targetSdkVersion": "26.0.0",
"runtimeOS": "HarmonyOS"
}

这组配置使用 API 26 SDK 编译,并以 API 26 为目标版本,最低兼容 API 24。本次验证设备是 TLR-AL00,系统 7.0.0.109(SP8C00E105R3P2),API 26,满足最低兼容要求。
这里的
6.1.1(24)不是随手填的值。适配工程最初沿用了模板里的26.0.0,在 API 24 的机器上安装会直接被拒(install failed due to older sdk version in the device,错误码9568297)。改成按设备 API 声明下限后,API 24 与 API 26 的设备都能装。
三、从源码仓库开始准备适配工程
3.1 将上游源码同步到 AtomGit
适配已有第三方插件时,先从 Pub 包信息或项目 README 确认上游地址、包名、版本和许可证。同步仓库时保留源码、许可证和提交历史,方便后续跟踪上游更新。
这个库的确认过程比通常要多一步:pub.dev 上 iana_time_zone 页面只有一行描述和用法示例,没有填写 homepage 或 repository 字段,README 里也没有仓库链接。最终是通过包描述、Android 包名 com.smile.iana_time_zone 和提交内容比对,定位到上游为 ds914048/iana_time_zone(GPL-3.0,3 个提交,最新 e9cc626)。
这一点值得单独记一笔:仓库里早期的 README.OpenSource 把上游写成了另一个不存在的地址,用 git ls-remote 一查就是 404。适配文档里的上游地址、许可证、版本号都属于可验证事实,不能凭记忆填。
在 AtomGit 网页的新建仓库流程中,找到从已有仓库导入的入口,填写上游 Git 地址,选择自己有写权限的组织或个人空间,设置目标仓库名称后执行导入。完成后检查目标分支、pubspec.yaml、LICENSE 和提交历史是否完整,并记录适配起点的提交号。如果上游本身托管在 AtomGit,也可以通过 Fork 获得自己的工作仓库。
iana_time_zone 现可从 AtomGit 获取。下面使用 AtomGit 地址拉取代码;需要提交修改时,使用自己有写权限的仓库或 Fork。
3.2 将代码拉取到宿主机
在安装了 Flutter OH 和 DevEco Studio 的开发电脑上打开终端,进入准备存放项目的目录,执行:
git clone https://atomgit.com/oh-flutter/iana_time_zone.git
cd iana_time_zone
pwd
ls
git remote -v
git status --short --branch
git rev-parse HEAD
git clone 会创建 iana_time_zone/ 目录;cd 后的位置就是下文所说的插件仓库根目录,这里应能看到 pubspec.yaml、lib/ 和 example/。
需要使用与本文相同的代码版本时,在没有未提交修改的仓库中切换到以下提交:
git switch --detach 7f93ee48bdfc47bcd32caaa1d0cf07aa95582710
适配其他插件时,使用该插件对应版本的 tag 或 commit 作为分支起点。

图 1:在宿主机终端输入 AtomGit 仓库拉取命令,并核对上游提交与许可证。
上游一共 3 个提交,全部发生在 2024 年 9 月 12 日这一天(first commit、Update LICENSE、Update README.md),之后没有更新。这种情况下“跟随上游”的工作量很小,适配分支可以只看自己的改动。
3.3 在仓库根目录创建适配分支
接着在 iana_time_zone/ 根目录创建分支。分支名统一使用 feat/ohos_库名称_版本号,库名称取 pubspec.yaml 的 name,版本号取此次适配的基线版本。本例为:
git switch -c feat/ohos_iana_time_zone_0.0.1
git branch --show-current
如果该分支已存在,使用 git switch feat/ohos_iana_time_zone_0.0.1 切换即可。

图 2:在 iana_time_zone 仓库根目录输入适配分支创建命令,并核对 TAG 与远端。
远端有两份:github 指向上游 ds914048/iana_time_zone,origin 指向 AtomGit 上的 oh-flutter/iana_time_zone。这样 git log github/main 就能随时确认自己落后上游多少。
3.4 自动补全 OHOS 适配结构
分支创建后,仍在同一个插件根目录执行结构补全。以下命令适用于尚无 ohos/ 目录的既有 Flutter 平台插件:
flutter create --template=plugin --platforms=ohos --project-name iana_time_zone .
git status --short
git diff -- pubspec.yaml lib example
--template=plugin指定插件模板。--platforms=ohos指定需要补全的平台。--project-name iana_time_zone使用 Dart 包名,与pubspec.yaml的name保持一致。- 最后的
.表示在当前插件目录补全工程,不是另建一层目录。
该命令生成 OHOS 平台脚手架,业务逻辑需要在 ArkTS 中实现。生成后通过 diff 检查 pubspec.yaml、lib/ 和 example/ 的变化,保留已有 API、其他平台注册项及依赖配置。不同 Flutter OH 版本生成的模板可能略有差异。
有一个容易踩的细节:flutter create --platforms=ohos . 对已有工程只追加 flutter.plugin.platforms.ohos 段,不会补 plugin_platform_interface 之类的依赖。本库的 Dart 代码没有用到 PlatformInterface,所以这一步没有引出问题;但换到用平台接口分层封装的库时,flutter analyze 会直接报 undefined_identifier。
如果生成后 example/ohos/ 仍不存在,进入已有示例应用补全平台:
cd example
flutter create --platforms=ohos .
cd ..
配套仓库已经包含 ohos/ 和 example/ohos/,直接运行示例时可以跳过结构补全。新建插件则使用 flutter create --org com.nutpi --template=plugin --platforms=ohos iana_time_zone;已有插件使用上面的 . 在当前目录补全。

图 3:补全后在仓库内核对通道名、平台判断与系统 API 调用。
3.5 适配后的项目目录
适配后的关键目录如下:
iana_time_zone/
├── lib/
│ └── iana_time_zone.dart
├── ohos/
│ ├── index.ets
│ ├── oh-package.json5
│ └── src/main/
│ ├── ets/components/plugin/IanaTimeZonePlugin.ets
│ └── module.json5
├── example/
│ ├── lib/main.dart
│ ├── test/widget_test.dart
│ └── ohos/entry/
├── README.OpenHarmony_CN.md
├── README.OpenHarmony.md
├── CHANGELOG.OpenHarmony.md
└── pubspec.yaml
项目根目录如下,其中包含 ohos/、example/,以及 OpenHarmony 中英文说明和变更记录文件:

图 4:适配后的 iana_time_zone 项目根目录。
| 文件 | 主要职责 |
|---|---|
lib/iana_time_zone.dart | 全部公开 API:一个静态 getter |
IanaTimeZonePlugin.ets | 注册 Flutter 通道并调用 HarmonyOS 系统时区接口 |
插件 module.json5 | 声明 HAR 模块与设备类型 |
示例 entry module.json5 | 声明宿主应用 Ability 与设备类型 |
example/lib/main.dart | 展示时区取值、实时时钟、调用统计与检测历史 |
example/test/widget_test.dart | 用 mock 通道验证取值与异常两条路径 |
README.OpenHarmony{,_CN}.md | 安装方式、接口表、已知问题与许可证说明 |
整个 Dart 层只有 15 行,ArkTS 侧 57 行,example/lib/main.dart 231 行,测试 51 行——这个体量也决定了本文的篇幅分配:接口分析很短,验证过程反而更值得展开。
四、Dart 接口与通道分析
OHOS 实现需要遵循 Dart 层已有的方法名和返回约定。先阅读 lib/iana_time_zone.dart,再在 ohos/src/main/ets/components/plugin/IanaTimeZonePlugin.ets 中实现对应的原生调用。适配其他已有平台的插件时,也应保留其公开接口和其他平台实现。
本例的对应关系如下:
| Dart 入口或模型 | 通道协议 | OHOS 实现 | 应保持的行为 |
|---|---|---|---|
IanaTimeZone.getIanaTimeZone | iana_time_zone / getIanaTimeZone | systemDateTime.getTimezoneSync() | 返回 IANA 时区 ID 字符串,与 Android/iOS 同语义 |
| 平台判断 | 无 | 无(Dart 侧判定) | ohos 必须被放行,否则直接返回 null |
| 异常 | result.error | IANA_TIME_ZONE_ERROR | Dart 侧以 PlatformException 抛出 |
原生端需要保持方法名 getIanaTimeZone 与通道名 iana_time_zone 完全一致。系统接口不可用时,通过错误回调通知 Dart 层,而不是返回空字符串。
4.1 跨端架构与调用时序
与双通道插件不同,这里只有一条 MethodChannel('iana_time_zone'):
命令只有一次往返,没有后续推送。因此页面不需要处理“订阅成功但事件还没来”的中间态,只要区分“加载中 / 有值 / 有错误”三种情况即可。
4.1.1 一次取值的时序
getTimezoneSync 是同步接口,ArkTS 侧拿到字符串后立刻回包,序列图和实际执行顺序一致:没有 await,也没有回调注册。
4.2 返回值模型:一个字符串,没有枚举
上游没有定义状态码或枚举,返回值就是 IANA 名称本身。业务侧通常这样使用:
| 返回值 | 含义 | 典型用途 |
|---|---|---|
Asia/Shanghai | 中国标准时间 | 给 DateFormat 指定时区、日志标注 |
Australia/Adelaide | 澳大利亚中部时间(UTC+10:30) | 验证“跟随系统设置”而非硬编码 |
null | 当前平台不在放行名单内 | 说明平台判断没覆盖到 |
null 是这里唯一需要小心处理的值。它不是错误,也不是“读取失败”,而是“这个平台我不打算支持”。适配 OHOS 时如果不改判断条件,拿到的就是它,而且没有任何异常可供排查。
4.3 公开 API 与平台接口
整个 Dart 文件只有 15 行,完整内容如下:
import 'dart:io';
import 'package:flutter/services.dart';
class IanaTimeZone {
static const MethodChannel _channel = MethodChannel('iana_time_zone');
static Future<String?> get getIanaTimeZone async {
if(Platform.isAndroid||Platform.isIOS||Platform.operatingSystem == 'ohos') {
final String timeZoneId = await _channel.invokeMethod('getIanaTimeZone');
return timeZoneId;
}
return null;
}
}
适配时只改了这一行的判断条件:
if(Platform.isAndroid||Platform.isIOS||Platform.operatingSystem == 'ohos') {
其余逻辑零改动。之所以不用 Platform.isOhos,是因为本 Flutter OHOS fork 的 dart:io 并没有提供这个成员,只能用 Platform.operatingSystem 的字符串判断。
这个判断的位置也有讲究:operatingSystem == 'ohos' 必须放在 isLinux 之类的分支之前。OHOS 的内核是 Linux,如果代码里写成 if (Platform.isLinux) { ... } else if (operatingSystem == 'ohos') { ... },鸿蒙设备会先命中 Linux 分支,走错实现。
4.4 Dart 通道协议分析
4.4.1 通道名称必须两端完全一致
static const MethodChannel _channel = MethodChannel('iana_time_zone');
this.channel = new MethodChannel(binding.getBinaryMessenger(), 'iana_time_zone');
通道名称属于跨语言协议。Dart 和 ArkTS 任何一端拼写不一致,都会出现“方法未实现”的表现:Dart 侧收到 MissingPluginException,ArkTS 侧根本没有被调用。
4.4.2 方法名与参数
Dart 侧调用不带参数:
final String timeZoneId = await _channel.invokeMethod('getIanaTimeZone');
ArkTS 侧按 call.method 分支,不读 call.arguments:
if (call.method === 'getIanaTimeZone') {
const timezone: string = systemDateTime.getTimezoneSync();
result.success(timezone);
} else {
result.notImplemented();
}
invokeMethod 的返回值类型是 dynamic,Dart 侧用 final String timeZoneId = ... 隐式断言为字符串。如果原生返回 null 而这里声明为 String,会抛类型错误——所以本库的 ArkTS 实现里没有“读不到就返回 null”的分支,读不到就直接走异常路径。
4.4.3 错误回传
} catch (e) {
result.error('IANA_TIME_ZONE_ERROR', describeError(e), null);
}
result.error 的第一个参数是错误码,第三个参数是附加信息。Dart 侧收到后包装成 PlatformException,code 为 IANA_TIME_ZONE_ERROR,message 为 describeError(e) 的结果。示例页把这两项直接拼到界面上,排查时不需要额外加日志。
五、补全 OHOS 原生实现与工程配置
5.1 在 IanaTimeZonePlugin.ets 中实现原生能力
以 getIanaTimeZone 为例:业务仍调用 IanaTimeZone.getIanaTimeZone,Dart 通道仍发送同名命令。需要补全的是 IanaTimeZonePlugin.ets 中的对应分支:调用 systemDateTime.getTimezoneSync() 读取系统时区,通过 result.success 或 result.error 回传结果。
IanaTimeZonePlugin 实现 FlutterPlugin 和 MethodCallHandler。下面列出类中的主要成员和方法,完整文件还包含 getUniqueClassName() 等插件接口。
原生插件位于:
ohos/src/main/ets/components/plugin/IanaTimeZonePlugin.ets
5.1.1 引入 Flutter 和 HarmonyOS 能力
import {
FlutterPlugin,
FlutterPluginBinding,
MethodCall,
MethodCallHandler,
MethodChannel,
MethodResult,
} from '@ohos/flutter_ohos';
import systemDateTime from '@ohos.systemDateTime';
import { BusinessError } from '@ohos.base';
其中:
FlutterPlugin负责接入 Flutter Engine 生命周期;MethodChannel接收 Dart 发来的命令;MethodResult回传结果或错误;systemDateTime提供getTimezoneSync(),返回 IANA 时区 ID;BusinessError用于读取 HarmonyOS 异常码和异常消息。
这里没有 EventChannel、EventSink,也没有 hilog:本插件只在被调用时读一次系统值,没有需要持续观察的状态,日志价值也不如直接把异常原文回传给 Dart。
5.1.2 连接 Flutter Engine
private channel?: MethodChannel;
onAttachedToEngine(binding: FlutterPluginBinding): void {
this.channel = new MethodChannel(binding.getBinaryMessenger(), 'iana_time_zone');
this.channel.setMethodCallHandler(this);
}
getBinaryMessenger() 拿到的 messenger 与 Dart 侧的通道一一对应。setMethodCallHandler(this) 把本类注册为命令处理器,因此类需要实现 MethodCallHandler 的 onMethodCall。
5.1.3 读取系统时区
onMethodCall(call: MethodCall, result: MethodResult): void {
try {
if (call.method === 'getIanaTimeZone') {
const timezone: string = systemDateTime.getTimezoneSync();
result.success(timezone);
} else {
result.notImplemented();
}
} catch (e) {
result.error('IANA_TIME_ZONE_ERROR', describeError(e), null);
}
}
getTimezoneSync() 是同步接口,返回 Asia/Shanghai 这类 IANA ID,语义与 Android 的 TimeZone.getDefault().id 一致。选同步版本而不是 getTimezone() 回调版,是因为通道回调本身就是异步的,回调里再套回调只会让错误处理更绕。
else 分支返回 notImplemented(),这样 Dart 调用其他方法名时会得到明确的 MissingPluginException,而不是静默超时。
5.1.4 把 BusinessError 解成可读文本
catch (e) 里的 e 在 ArkTS 中是 Object,直接写 `${e}` 得到的是 [object Object]——错误码和消息全丢了。所以这里单独包了一个函数:
/**
* BusinessError interpolates as "[object Object]" with a plain template
* string; unpack code and message explicitly.
*/
function describeError(e: Object): string {
const err = e as BusinessError;
if (err && typeof err.code === 'number' && typeof err.message === 'string') {
return `(${err.code}) ${err.message}`;
}
return `${e}`;
}
回传后的文本形如 (401) Invalid parameters。这个细节不影响功能,但直接决定排查效率:示例页把错误码和消息显示在卡片上,截图里能直接读到原因。
5.1.5 Engine 解绑时释放资源
onDetachedFromEngine(binding: FlutterPluginBinding): void {
if (this.channel) {
this.channel.setMethodCallHandler(null);
}
}
Flutter Engine 销毁时清掉 Handler,避免通道继续指向已销毁的插件实例。本插件没有系统监听可释放,这一步已经足够。
5.2 声明插件和宿主权限
systemDateTime.getTimezoneSync() 读取的是系统时区设置,属于基础系统能力,不需要申请任何权限:
- 不需要
ohos.permission.ACTIVITY_MOTION(那是运动状态); - 不需要
ohos.permission.LOCATION(时区不是定位,虽然系统设置里有“自动定位所在时区”选项,但那是系统行为); - 不需要在
module.json5里写requestPermissions。
所以插件的 ohos/src/main/module.json5 保持脚手架生成的最小内容:
{
"module": {
"name": "iana_time_zone",
"type": "har",
"deviceTypes": ["default", "tablet"]
}
}
对比一下需要感知能力的插件:它们必须在插件 HAR 和宿主的 entry 模块里各声明一次权限,还要补 string.json 里的原因文案;本库这两处都可以空着。“无须额外权限”本身就是一条需要确认的适配结论,不能因为模板里有权限章节就照搬。
5.3 注册并导出插件
pubspec.yaml 通过以下配置声明 OHOS 插件类:
flutter:
plugin:
platforms:
android:
package: com.smile.iana_time_zone
pluginClass: IanaTimeZonePlugin
ios:
pluginClass: IanaTimeZonePlugin
ohos:
pluginClass: IanaTimeZonePlugin
插件的 ohos/index.ets 需要导出实现:
import IanaTimeZonePlugin from './src/main/ets/components/plugin/IanaTimeZonePlugin';
export default IanaTimeZonePlugin;
执行 flutter pub get 和构建后,Flutter 工具会为应用生成插件注册代码。通常不应手工编辑 GeneratedPluginRegistrant.ets,因为下次构建可能覆盖它。
验证注册是否生效,最直接的办法是看启动日志:
FlutterEngineCxnRegistry --> Adding plugin: IanaTimeZonePlugin
DartMessenger --> Setting handler for channel 'iana_time_zone'
这两行出现在 hilog 里,说明插件已被引擎加载、通道也挂上了。缺前者是注册问题,缺后者是通道名写错或 onAttachedToEngine 没执行。
注册异常的排查步骤见第九节 MissingPluginException。
5.4 检查 example 的 OHOS 应用结构
本例的 example/ohos/build-profile.json5 应在 products 中设置版本。下面是需核对的配置片段,请合并到现有工程;签名材料保留在本地,公开仓库中提交的版本把 signingConfigs 置为空数组。
{
"app": {
"signingConfigs": [],
"products": [
{
"name": "default",
"compatibleSdkVersion": "6.1.1(24)",
"targetSdkVersion": "26.0.0",
"runtimeOS": "HarmonyOS"
}
]
},
"modules": [
{
"name": "entry",
"srcPath": "./entry",
"targets": [
{
"name": "default",
"applyToProducts": ["default"]
}
]
}
]
}
配置后,在 DevEco Studio 中执行一次 Sync Project。如果 entry 模块正常识别,Project 视图中会出现 entry,并能打开 entry 模块的签名配置。
提交版把
signingConfigs写成空数组,是为了不把本机证书路径、p12/p7b 文件和加密口令带进公开仓库。DevEco 的自动签名会把这些内容直接写进build-profile.json5,一旦提交,等于把签名材料连同口令一起公开。需要真机安装时,在本机临时恢复签名配置,装完再还原即可。
六、补全交付文件并提交适配分支
6.1 除代码外还要补全哪些文件
代码适配完成后,还需要整理安装说明、接口文档、版本记录和开源信息。接收仓库有专用模板时,按其格式填写:
| 文件 | 应写清楚的内容 |
|---|---|
README.OpenSource | 上游名称、源码地址、适配版本或提交、版权及许可证信息;按仓库模板列出第三方依赖 |
README.md | 原项目说明、OHOS 支持入口、配套 Demo 和文档链接;保留上游信息 |
README.OpenHarmony_CN.md | 简介、AtomGit 安装方式、版本对应关系、环境约束、权限、接口表、示例、已验证范围和遗留问题 |
README.OpenHarmony.md | 与中文说明对应的英文文档 |
CHANGELOG.OpenHarmony.md | OHOS 新增能力、适配版本、兼容限制与测试范围 |
LICENSE / NOTICE | 保留上游许可证;NOTICE 按许可证和原项目要求保留或补充 |
example/README.md | 依赖方式、运行目录、签名、操作步骤与效果图;覆盖取值与刷新 |
pubspec.yaml、ohos/oh-package.json5 | 核对包名、版本、插件注册、仓库地址、许可证和依赖 |
.gitignore | 忽略构建缓存及本机签名材料,不漏提交必要源码和配置 |
README.OpenSource 记录库本身的来源与版本。本例的包名为 iana_time_zone,版本为 0.0.1,采用 GPL-3.0 许可证——这一点需要注意:GPL-3.0 与常见的 Apache-2.0/MIT 不同,分发衍生作品时有传染性要求,接入方应自行确认合规性。
ohos/oh-package.json5 里的 license 字段脚手架默认写的是 Apache-2.0,与上游 LICENSE 不一致,适配时改成了 GPL-3.0。这类字段不影响编译,但会让依赖扫描工具得出错误结论。
分支表也要按实际分支写。本文仓库的默认分支是 main,早期文档里写的 master 是模板遗留,安装时按 master 找会报分支不存在。
6.2 提交前检查
提交前先完成第八节的插件、example 和真机测试,再从根目录检查改动:
git branch --show-current
git diff --check
git status --short
git diff --stat
git diff
检查 diff 中的接口、平台注册和依赖变化,移除本机路径及签名信息,并使文档中的版本和分支与提交内容一致。
6.3 提交并推送到 AtomGit
文档和代码整理完成后,在根目录暂存并提交。文件名按项目实际情况调整:
git add ohos pubspec.yaml example .gitignore
git add README.md README.OpenSource README.OpenHarmony_CN.md
git add README.OpenHarmony.md CHANGELOG.OpenHarmony.md
git diff --cached --check
git diff --cached --stat
git diff --cached
git commit -m "feat: add OHOS support for iana_time_zone 0.0.1"
git remote -v
git branch --show-current
git push -u origin feat/ohos_iana_time_zone_0.0.1
DevEco 可能向 example/ohos/build-profile.json5 写入本机签名配置,需要在提交前从暂存内容中移除。推送时,origin 应指向自己的 AtomGit 仓库,当前分支为 feat/ohos_iana_time_zone_0.0.1。
TAG 命名遵循 <上游版本>-ohos-1.0.0-beta.1。这个库的适配仓库早期打过一个 v0.0.1 的 TAG,名字看着像上游发布版,实际指向的是本地适配提交,而 git ls-remote --tags 查上游时是空的——也就是说,上游从未发过任何 TAG。这种“伪 TAG”会误导使用者以为对应官方版本,处理办法是删掉远端 v0.0.1,改打 0.0.1-ohos-1.0.0-beta.1:
git tag -d v0.0.1
git push origin :refs/tags/v0.0.1
git tag 0.0.1-ohos-1.0.0-beta.1 <适配提交> -m "iana_time_zone 0.0.1 OpenHarmony adaptation"
git push origin 0.0.1-ohos-1.0.0-beta.1
推送后在 AtomGit 发起合并请求,说明上游来源和版本、OHOS 实现范围、依赖及权限、测试环境、操作结果、已知限制,并附 Demo 运行图。目标分支和评审流程以接收仓库要求为准。
七、使用根目录 example 演示接入
仓库自带 example/,可以直接用来调试插件和查看当前设备的时区取值。
7.1 本地适配时使用路径依赖
当前 example/pubspec.yaml 的依赖是:
dependencies:
flutter:
sdk: flutter
iana_time_zone:
path: ../
../ 相对于 example/pubspec.yaml 指向插件根目录,修改根目录插件后可直接联调。
7.2 通过 AtomGit 引入插件
业务应用通过 AtomGit 引入时,将 iana_time_zone 的 path 配置替换为下面的 Git 依赖。这里固定到本文使用的提交:
dependencies:
flutter:
sdk: flutter
iana_time_zone:
git:
url: https://atomgit.com/oh-flutter/iana_time_zone.git
ref: 7f93ee48bdfc47bcd32caaa1d0cf07aa95582710
也可以直接写 TAG:
ref: 0.0.1-ohos-1.0.0-beta.1
使用自己的适配版本时,先推送分支,再将 url 改为对应仓库,ref 改为 feat/ohos_iana_time_zone_0.0.1。正式发布后可固定到 tag 或 commit。
从插件根目录执行:
cd example
flutter pub get
flutter pub deps
检查 example/pubspec.lock 中 iana_time_zone 的来源为 git,并核对 url、ref 和 resolved-ref。同时检查没有 dependency_overrides 或 pubspec_overrides.yaml 将其覆盖回本地依赖,确认应用使用的是 Git 依赖。
7.3 调用接口实现时区显示
下面的页面展示当前时区与刷新按钮,可用于 example/lib/main.dart。仓库中的完整 Demo 还提供实时时钟、UTC 偏移、调用统计和检测历史。
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:iana_time_zone/iana_time_zone.dart';
void main() {
runApp(const MaterialApp(home: IanaTimeZonePage()));
}
class IanaTimeZonePage extends StatefulWidget {
const IanaTimeZonePage({super.key});
State<IanaTimeZonePage> createState() => _IanaTimeZonePageState();
}
class _IanaTimeZonePageState extends State<IanaTimeZonePage> {
String? _timezone;
String? _error;
bool _loading = false;
void initState() {
super.initState();
_detect();
}
Future<void> _detect() async {
setState(() {
_loading = true;
_error = null;
});
try {
final tz = await IanaTimeZone.getIanaTimeZone;
if (!mounted) return;
setState(() => _timezone = tz);
} on PlatformException catch (e) {
if (!mounted) return;
setState(() => _error = '[${e.code}] ${e.message ?? '获取时区失败'}');
} finally {
if (mounted) setState(() => _loading = false);
}
}
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: const Text('IANA TimeZone')),
body: Padding(
padding: const EdgeInsets.all(16),
child: Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
if (_loading)
const CircularProgressIndicator()
else
Text(
_timezone ?? _error ?? 'null(当前平台不支持)',
style: Theme.of(context).textTheme.headlineSmall,
textAlign: TextAlign.center,
),
const SizedBox(height: 16),
FilledButton(
onPressed: _loading ? null : _detect,
child: const Text('重新获取时区'),
),
],
),
),
);
}
}
判空写法 _timezone ?? _error ?? 'null(当前平台不支持)' 是有意保留的:当平台判断没覆盖到 OHOS 时,_timezone 为 null、_error 也为 null,界面会明确显示“当前平台不支持”,而不是空白卡片。这类“什么都不显示”的现象最难排查,加一行兜底文案就能省掉一轮修改。
7.4 页面退出时不需要清理
异步回调先检查 mounted,避免页面销毁后继续调用 setState。与订阅型插件不同,这里没有事件流和系统监听,因此 dispose 中不需要取消任何东西——Dart 侧的 MethodChannel 是一次性调用,返回后即结束;ArkTS 侧也没有注册回调。
反过来说,如果应用需要“时区变化时自动更新”,本库不提供该能力:它只在被调用时读一次系统值。用户改了系统时区后,界面不会自己变,需要重新调用 getIanaTimeZone,或者监听系统时区变化的广播(@ohos.commonEvent 的 COMMON_EVENT_TIMEZONE_CHANGED,属于宿主应用自己要做的事)。
八、验证、构建与鸿蒙设备运行效果
8.1 分别验证插件与 example
从插件仓库根目录执行:
flutter pub get
flutter analyze
flutter test
cd example
flutter pub get
flutter analyze
flutter test
本次实际执行结果需要如实说明:
| 检查项 | 结果 |
|---|---|
根目录 flutter analyze | No issues found! |
example 目录 flutter analyze | No issues found! |
根目录 flutter test | Test directory "test" not found.(上游没有 test/ 目录) |
example 目录 flutter test | 未执行:命令启动后无任何输出,挂起至超时被终止 |
flutter test 在本机属于工具链问题,既有过 Invalid SDK hash 报错,也有本次这种无输出挂起,与插件代码无关。因此接口行为的证据来自两处:example/test/widget_test.dart 的契约测试代码本身,以及第八节的真机截图。
测试用 mock 通道覆盖两条路径:
setUp(() {
TestWidgetsFlutterBinding.instance.defaultBinaryMessenger
.setMockMethodCallHandler(channel, (call) async {
expect(call.method, 'getIanaTimeZone');
return 'Asia/Shanghai';
});
});
第一条用例断言页面渲染出 Asia/Shanghai、调用次数为 1、历史记录里有该值;第二条用例让通道抛 PlatformException,断言界面显示 [IANA_TIME_ZONE_ERROR] boom 而不是停在加载态。这正好对应适配中最容易出问题的两种情形:值传不回来、错误被吞掉。
接口测试应验证取值转发、错误回调以及平台判断的行为。Widget 测试应匹配实际保留的 Demo 页面;如果替换成第七节最小页面,也要相应调整原来的 UI 断言。
8.2 确认设备连接
hdc list targets
flutter devices
设备首次连接电脑时,需要在手机端确认调试授权。列表为空时,检查 USB 连接、调试模式和电脑授权。
本次验证设备信息:
型号:TLR-AL00
系统:7.0.0.109(SP8C00E105R3P2)
API:26
8.3 配置签名
真机安装的 HAP 通常需要有效签名。推荐使用 DevEco Studio 为 entry 模块配置自动签名:
- 用 DevEco Studio 打开
example/ohos,不是仓库根目录; - 等待工程 Sync 成功,确认 Project 视图中存在
entry模块; - 打开 File > Project Structure > Signing Configs;
- 为
defaultproduct 选择或生成签名; - 确认设备、应用包名、证书和 Profile 匹配;
- 再回到终端执行 Flutter 构建或运行。
签名材料保存在本机,公开仓库中只保留构建所需的通用配置。
8.4 运行示例
以下命令在 example/ 目录执行,将 <device-id> 替换为设备列表中的实际 ID:
flutter run -d <device-id>
也可以先构建 HAP:
flutter build hap --debug
本次没有走 flutter build hap,而是直接调用 DevEco 自带的 hvigor 任务——它和前者最终执行的是同一套 assembleHap,但绕开了本机上 Flutter 封装层在 hvigor 阶段提前退出的问题:
# 1. 在 example/ohos 里装依赖(PowerShell 下调 ohpm.bat)
ohpm.bat install
# 2. 直接调 DevEco 的 hvigorw(清掉 NODE_OPTIONS,JAVA_HOME 指向 DevEco 自带的 jbr)
hvigorw.bat assembleHap -p product=default -p buildMode=debug --no-daemon
构建输出 BUILD SUCCESSFUL,随后即使命令以退出码 1 结束也可以继续——那是尾部脚本调用 wmic 被安全策略拦下导致的,不影响产物。
典型产物位于:
example/ohos/entry/build/default/outputs/default/
目录中通常包含已签名和未签名 HAP:
entry-default-signed.hap
entry-default-unsigned.hap
真机安装应选择与当前设备匹配的已签名产物。用命令行安装:
hdc install -r entry-default-signed.hap
8.5 在设备上测试取值和刷新
- 打开应用,确认首屏显示一个 IANA 时区名(如
Asia/Shanghai); - 记录下方“本地时间”和“UTC 偏移”,确认与系统时间一致;
- 点击“重新获取时区”,确认调用次数递增、检测历史新增一条;
- 连续点击几次,确认历史最多保留 5 条、不会重复渲染;
- 到系统设置里改一次时区,回到应用重新获取,确认返回值随之变化;
- 恢复系统时区(重新打开“自动设置”),再次获取,确认回到原值;
- 全程观察是否出现
null(当前平台不支持)或异常卡片。
第 5、6 步是这次适配最关键的验证:它区分了“真的读了系统时区”和“某个环节缓存/写死了值”。
8.6 鸿蒙设备运行效果
完成适配后,Flutter 应用可以通过一行 await IanaTimeZone.getIanaTimeZone 拿到设备当前的 IANA 时区。
首屏效果如下——大卡片显示 Asia/Shanghai,下方是本地时间与 UTC+08:00:
| 首次进入 | 连续两次刷新后 |
|---|---|
调用次数 1,历史 1 条 | 调用次数 3,历史 3 条 |
点击“重新获取时区”两次后,调用统计 变为 getIanaTimeZone 已调用 3 次,检测历史 按时间倒序列出三条记录。这说明每次点击都真实走了一次跨端调用,而不是读本地缓存。
页面底部是本次适配的说明卡片:
卡片里写清了三件事:OHOS 侧用 systemDateTime.getTimezoneSync();返回值是 IANA 时区 ID,与 Android TimeZone.getDefault().id 一致;非 Android/iOS/OHOS 平台上游会直接返回 null。这三行对应第一节讲的适配目标,也对应 FAQ 9.4 的排查线索。
8.6.1 用系统设置做对照实验
只看一个 Asia/Shanghai 无法排除“值被写死”的可能。为此做了一次对照:先记录系统时区,再改系统时区,再看应用返回值。
系统设置中,时区显示为 GMT+08:00 中国标准时间,与应用的 Asia/Shanghai、UTC+08:00 对应:
| 系统设置 | 应用返回值 |
|---|---|
GMT+08:00 中国标准时间 | Asia/Shanghai,UTC+08:00 |
GMT+10:30 澳大利亚中部时间 | Australia/Adelaide,UTC+10:30 |
关闭“自动设置”后手动选择阿德莱德,重新启动应用,卡片直接变成 Australia/Adelaide,本地时间与偏移同步变为 12:18:19 和 UTC+10:30:
改回“自动设置”后系统时区恢复为 GMT+08:00,重新启动应用,返回值回到 Asia/Shanghai:
这一步同时验证了三件事:插件读的是系统当前值、切换后无需重启进程也会生效(重新取值即可)、恢复系统设置后不会残留上一次的结果。
8.6.2 启动与调用日志
hilog 中能看到插件注册与通道调用:
FlutterEngineCxnRegistry --> Adding plugin: IanaTimeZonePlugin
DartMessenger --> Setting handler for channel 'iana_time_zone'
FlutterNapi --> called handlePlatformMessage Channel: iana_time_zone. Response ID: 8
DartMessenger --> Received message from Dart over channel 'iana_time_zone'
四条日志分别对应:插件被引擎加载、通道挂上 Handler、Dart 发起调用、原生收到消息。页面上的调用次数与日志条数能对上,说明“点击一次 = 一次跨端往返”。
不同型号设备的系统时区数据库版本可能不同,同一地区的 ID 也可能有别名(如 Asia/Calcutta 与 Asia/Kolkata)。业务侧比较时区时建议按偏移和规则判断,不要直接字符串相等。
九、FAQ:适配过程与使用问题
9.1 Missing SDK components
典型错误如下:
Missing SDK components. SDK path: ...,
missing components: toolchains,ets,js,native,previewer.
这个错误发生在 Hvigor 同步阶段。通常需要检查构建工具使用的 SDK 路径、组件是否完整,以及 Hvigor 与 SDK 的版本是否匹配。
处理顺序:
- 在 DevEco Studio SDK Manager 中确认 API 26 组件已经下载完整;
- 检查 Flutter 和 DevEco Studio 使用的 SDK 路径是否一致;
- 避免误用
/Applications/DevEco-Studio.app/Contents/sdk之类的不完整目录; - 确认 SDK 根目录下存在
toolchains、ets、js、native、previewer; - 执行
flutter config --ohos-sdk <正确路径>; - 重新执行
flutter doctor -v和 DevEco Studio Sync。
因为 Sync 和 Compile 首先读取本机 SDK。手机 API 版本只在部署、安装和运行时参与兼容判断。即使完全不连接手机,本地 SDK 不完整时也会得到相同错误。
当前工程的 compatibleSdkVersion 是 API 24,因此 API 24 与 API 26 的设备在安装版本门槛上都满足;但系统时区接口属于基础能力,仍需在目标设备上确认返回值正确。
9.2 DevEco Studio 中看不到 entry 模块
插件的 ohos/ 目录是 HAR 模块,可安装应用的 entry 模块位于 example/ohos/entry。
请直接使用 DevEco Studio 打开:
iana_time_zone/example/ohos
如果仍看不到 entry,先解决 SDK Sync 错误,再检查 example/ohos/build-profile.json5 的 modules 是否包含 ./entry。同步失败时,Project Structure 无法正确解析模块,签名界面也可能不显示 entry。
9.3 无法手动签名
签名配置依附于可构建的应用模块和 product。只有 HAR 插件模块、工程 Sync 失败,或者打开了错误目录时,DevEco Studio 都可能无法提供 entry 签名入口。
建议先确认:
- 打开的是
example/ohos; - SDK 组件完整并且 Sync 成功;
entry的模块类型为entry;defaultproduct 和 target 已正确关联;- 当前账号、证书和调试设备状态有效。
9.4 应用一直显示 null(当前平台不支持)
这是本库最典型的适配故障,原因只有一个:Dart 层的平台判断没有放行 ohos。
// 上游写法:OHOS 上永远走不到 invokeMethod
if (Platform.isAndroid || Platform.isIOS) { ... }
return null;
因为返回的是 null 而不是抛错,try/catch 也接不住,界面表现就是“卡片一直是空的或显示不支持”。修改方式:
if (Platform.isAndroid || Platform.isIOS || Platform.operatingSystem == 'ohos') { ... }
注意不要用 Platform.isOhos(本 fork 未提供该成员),也不要把判断写在 Platform.isLinux 之后——OHOS 内核是 Linux,顺序写反会命中错误分支。
按以下顺序检查:
- 界面是否显示
null(当前平台不支持)而不是错误卡片; lib/iana_time_zone.dart的判断是否包含Platform.operatingSystem == 'ohos';hilog中是否出现Received message from Dart over channel 'iana_time_zone'(没有就是没走到通道);- 应用是否用的 Git 依赖旧提交(
pubspec.lock的resolved-ref); - 改完 Dart 代码后是否重新构建(热重载不会重新注册原生插件)。
9.5 MissingPluginException
这通常表示 Dart 通道找不到已注册的原生插件。新增原生插件后需要重新构建应用。从仓库根目录执行:
cd example
flutter clean
flutter pub get
flutter run -d <device-id>
如果仍然出现,检查自动生成的插件注册文件中是否包含 IanaTimeZonePlugin,同时核对 pubspec.yaml、ohos/index.ets 和 oh-package.json5。还要确认 ohos/index.ets 的默认导出确实是插件类——导出路径写错时编译能过,注册却拿不到实现。
9.6 编译成功但安装失败(9568297)
本库适配过程中遇到过一次:
code:9568297
error: install failed due to older sdk version in the device
含义是安装包的 compatibleSdkVersion 高于设备 API。工程模板里写的是 26.0.0,而设备是 API 24,于是被拒。把 compatibleSdkVersion 按设备能力改为 6.1.1(24) 后即可安装,同一份 HAP 在 API 26 的设备上也能装。
其他常见原因包括:
- HAP 未签名或使用了错误的 Profile;
- 设备未加入调试设备列表;
- 包名与签名 Profile 不匹配;
- 手机上已经安装了使用不同证书签名的同包名应用。
根据安装错误码区分签名、版本和包名冲突,再处理对应配置。
9.7 flutter create 不认识 ohos,或包名不合法
先执行 flutter --version,确认使用的是 OHOS 版工具链,环境配置回到第二节的社区链接核对。包名报错时,确认当前目录包含目标插件的 pubspec.yaml,并显式传入 --project-name iana_time_zone。生成后检查 diff,再补充 ArkTS 业务实现。
9.8 AtomGit 依赖提示找不到分支或无权限
先检查 URL 是否指向已同步的目标仓库,再确认 feat/ohos_iana_time_zone_0.0.1 已推送。分支尚未推送时,可以先使用第七节的提交号。私有仓库还需在本机配置 Git 认证。
另外注意 TAG 名:这个库的适配 TAG 是 0.0.1-ohos-1.0.0-beta.1,不是 v0.0.1——后者曾是本地伪 TAG,已从远端删除。ref 写错时 flutter pub get 会报 Could not find ref。
9.9 改了本地 ArkTS,Demo 为什么没变化
先检查 example/pubspec.yaml:Git 依赖读取远程提交,不会自动读取本地插件改动。本地联调切回 path: ../;测试远程版本则先提交推送,再更新依赖并核对 pubspec.lock 的 resolved-ref。原生代码变动后停止应用并重新构建运行,不能只做 Dart 热重载。
9.10 错误信息显示 [object Object]
早期 ArkTS 实现里写的是:
} catch (e) {
result.error('IANA_TIME_ZONE_ERROR', `${e}`, null);
}
e 的类型是 Object,模板字符串直接插值得到 [object Object],错误码和消息全丢。改为按 BusinessError 解包:
function describeError(e: Object): string {
const err = e as BusinessError;
if (err && typeof err.code === 'number' && typeof err.message === 'string') {
return `(${err.code}) ${err.message}`;
}
return `${e}`;
}
之后再遇到 MissingPluginException 之外的异常,界面会直接显示 (错误码) 消息,不必再回到 hilog 里翻。
9.11 在模拟器或桌面上运行返回 null
iana_time_zone 只放行 Android、iOS 和 OHOS 三个平台。在 Flutter Web、桌面(Windows/macOS/Linux)上运行,getIanaTimeZone 返回 null 属于设计行为,不是适配缺陷。
如果业务需要覆盖更多平台,可以在 Dart 侧自己兜底(例如用 DateTime.now().timeZoneName 作为降级值),但要注意它返回的可能是缩写(CST)而不是 IANA 名称,语义与插件不同,不能直接混用。
相关链接
更多推荐




所有评论(0)