开发工具: 华为云码道

原始仓库: 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 SDK3.47.7-ohos-1.0.0Flutter 编译与 OHOS 平台工具链
Flutter 分支oh-3.47-dev对应的开发分支
Dart SDK3.13.3Dart 语言与包管理环境
DevEco Studio26.0.0(26.0.0.851)打开 example/ohos、签名与构建
HarmonyOS 开发套件26.0.0(API 26)开发套件版本及对应的 API 级别
targetSdkVersion26.0.0应用面向的行为版本
compatibleSdkVersion6.1.1(24)当前工程声明的最低兼容版本
插件版本0.0.1pubspec.yaml 中的包版本
原生语言ArkTSHarmonyOS 插件实现
插件产物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.getIanaTimeZoneiana_time_zone / getIanaTimeZonesystemDateTime.getTimezoneSync()返回 IANA 时区 ID 字符串,与 Android/iOS 同语义
平台判断无无(Dart 侧判定)ohos 必须被放行,否则直接返回 null
异常result.errorIANA_TIME_ZONE_ERRORDart 侧以 PlatformException 抛出

原生端需要保持方法名 getIanaTimeZone 与通道名 iana_time_zone 完全一致。系统接口不可用时,通过错误回调通知 Dart 层,而不是返回空字符串。

4.1 跨端架构与调用时序

与双通道插件不同,这里只有一条 MethodChannel('iana_time_zone'):

android / ios / ohos

其他平台

Flutter 页面

IanaTimeZone.getIanaTimeZone

Platform 判断

MethodChannel iana_time_zone

返回 null

ArkTS IanaTimeZonePlugin

systemDateTime.getTimezoneSync

命令只有一次往返,没有后续推送。因此页面不需要处理“订阅成功但事件还没来”的中间态,只要区分“加载中 / 有值 / 有错误”三种情况即可。

4.1.1 一次取值的时序
HarmonyOS systemDateTime IanaTimeZonePlugin.ets IanaTimeZone Flutter App HarmonyOS systemDateTime IanaTimeZonePlugin.ets IanaTimeZone Flutter App await IanaTimeZone.getIanaTimeZone Platform.isAndroid || isIOS || operatingSystem == 'ohos' invokeMethod('getIanaTimeZone') getTimezoneSync() 'Asia/Shanghai' result.success('Asia/Shanghai') 'Asia/Shanghai'

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.mdOHOS 新增能力、适配版本、兼容限制与测试范围
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 analyzeNo issues found!
example 目录 flutter analyzeNo issues found!
根目录 flutter testTest 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 模块配置自动签名:

  1. 用 DevEco Studio 打开 example/ohos,不是仓库根目录;
  2. 等待工程 Sync 成功,确认 Project 视图中存在 entry 模块;
  3. 打开 File > Project Structure > Signing Configs;
  4. 为 default product 选择或生成签名;
  5. 确认设备、应用包名、证书和 Profile 匹配;
  6. 再回到终端执行 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 在设备上测试取值和刷新

  1. 打开应用,确认首屏显示一个 IANA 时区名(如 Asia/Shanghai);
  2. 记录下方“本地时间”和“UTC 偏移”,确认与系统时间一致;
  3. 点击“重新获取时区”,确认调用次数递增、检测历史新增一条;
  4. 连续点击几次,确认历史最多保留 5 条、不会重复渲染;
  5. 到系统设置里改一次时区,回到应用重新获取,确认返回值随之变化;
  6. 恢复系统时区(重新打开“自动设置”),再次获取,确认回到原值;
  7. 全程观察是否出现 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 的版本是否匹配。

处理顺序:

  1. 在 DevEco Studio SDK Manager 中确认 API 26 组件已经下载完整;
  2. 检查 Flutter 和 DevEco Studio 使用的 SDK 路径是否一致;
  3. 避免误用 /Applications/DevEco-Studio.app/Contents/sdk 之类的不完整目录;
  4. 确认 SDK 根目录下存在 toolchains、ets、js、native、previewer;
  5. 执行 flutter config --ohos-sdk <正确路径>;
  6. 重新执行 flutter doctor -v 和 DevEco Studio Sync。
为什么连接 API 24 手机仍然会报这个错误?

因为 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;
  • default product 和 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,顺序写反会命中错误分支。

按以下顺序检查:

  1. 界面是否显示 null(当前平台不支持) 而不是错误卡片;
  2. lib/iana_time_zone.dart 的判断是否包含 Platform.operatingSystem == 'ohos';
  3. hilog 中是否出现 Received message from Dart over channel 'iana_time_zone'(没有就是没走到通道);
  4. 应用是否用的 Git 依赖旧提交(pubspec.lock 的 resolved-ref);
  5. 改完 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 名称,语义与插件不同,不能直接混用。


相关链接

Logo

作为“人工智能6S店”的官方数字引擎,为AI开发者与企业提供一个覆盖软硬件全栈、一站式门户。

更多推荐