Flutter 三方库 flutter_native_timezone 的 OpenHarmony 适配实战(0-1)
本文记录了将开源 Flutter 三方库
flutter_native_timezone从零适配到 OpenHarmony / HarmonyOS 平台的完整过程,
包含上游代码基线替换、OHOS 平台脚手架生成、ArkTS 原生实现、构建验证、真机运行与踩坑复盘。
一、背景
1.1 三方库简介
flutter_native_timezone 是一个 Flutter 社区广泛使用的设备时区获取插件,提供以下能力:
- getLocalTimezone()— 获取设备当前设置的本地时区(IANA 格式,如
Asia/Shanghai) - getAvailableTimezones()— 获取系统支持的可用时区 ID 列表(同样为 IANA 格式)
该三方库最初支持 Android / iOS / macOS / Web 四个平台,本次任务将其适配到OpenHarmony / HarmonyOS平台。
项目地址:oh-flutter/flutter_native_timezone
1.2 适配目标
| 维度 | 要求 |
|---|---|
| 功能一致性 | getLocalTimezone/getAvailableTimezones返回 IANA 时区 ID,语义与 Android / iOS 完全一致 |
| Dart 层零改动 | Dart API 与方法通道契约保持不变,仅新增 OHOS 平台实现 |
| 工程规范 | 遵循 OHOS 插件工程规范,SDK / 版本信息动态读取、不硬编码,中英文 README 同步 |
| 交付质量 | 完成一致性代码检查与真机验证,输出 0-1 适配过程复盘 |
二、适配路线图
整个适配分为 5 个阶段:
第 1 阶段:项目初始化 ── 以上游 pinkfish master 为干净基线,生成 ohos 平台脚手架 第 2 阶段:原生实现 ── 对照 Android/iOS 实现,用 ArkTS 完成 MethodChannel 方法 第 3 阶段:三方库注册 ── pubspec.yaml 注册 ohos 平台 + Dart SDK 约束升级 第 4 阶段:示例验证 ── 生成 example/ohos 宿主工程并构建签名 HAP 第 5 阶段:真机验证 ── 安装运行、代码一致性检查、双语文档输出三、逐步适配过程
第 1 阶段:项目初始化
本仓库此前的内容混杂了旧分支的历史与一次早期适配,为获得干净的 0-1 起点,先以官方上游pinkfish/flutter_native_timezone(master,pubspec 2.0.1)为基线重置:
gitremoteaddupstream https://github.com/pinkfish/flutter_native_timezone.gitgitfetch upstream mastergitreset--hardupstream/master# main 指向 fcc1f3f随后使用 Flutter OHOS 工具链生成 OHOS 插件模板:
flutter create.--template=plugin--platforms=ohos--orgcom.whelksoft --no-pub踩坑:仓库同时存在
com.whelksoft(插件)与com.example(示例)两个组织名时,flutter create会报Ambiguous organization,必须显式传入--org。
此外flutter create会按当前工具链的现代模板追加一批非目标文件(Gradle kts、federated 骨架、iOS Swift 副本等),需逐一甄别删除,只保留ohos/、example/ohos/及必要的 pubspec 变更。
该命令自动生成ohos/目录的标准模板结构:
ohos/ ├── index.ets # 模块入口,导出插件类 ├── oh-package.json5 # HAR 包配置 ├── build-profile.json5 # 构建配置 ├── local.properties # 本地 SDK/Flutter 路径(不入库) ├── src/main/ │ ├── module.json5 # HAR 模块配置 │ └── ets/components/plugin/ │ └── FlutterNativeTimezonePlugin.ets # 原生插件实现(核心)关键配置文件:
index.ets(入口导出)
importFlutterNativeTimezonePluginfrom'./src/main/ets/components/plugin/FlutterNativeTimezonePlugin';exportdefaultFlutterNativeTimezonePlugin;oh-package.json5(包配置)
{ "name": "flutter_native_timezone", "version": "1.0.0", "description": "A flutter plugin for getting the local timezone of the device.", "main": "index.ets", "author": "nutpi", "license": "Apache-2.0", "dependencies": {} }注:
@ohos/flutter_ohos由 Flutter OHOS 引擎在构建时通过har/flutter.har自动链接(flutter_tools会把@ohos/flutter_ohos覆写为引擎产物),插件dependencies无需显式声明。
module.json5(HAR 模块配置)
{ "module": { "name": "flutter_native_timezone", "type": "har", "deviceTypes": ["default", "tablet"] } }第 2 阶段:原生实现(核心)
本插件属于典型的方法调用型插件(Dart 通过MethodChannel主动调用原生),无需事件流、无需AbilityAware。
2.1 架构与通道契约
| 平台 | 插件类 | 通道名 | 编解码 |
|---|---|---|---|
| Android | FlutterNativeTimezonePlugin : FlutterPlugin, MethodCallHandler | flutter_native_timezone | StandardMethodCodec |
| iOS | FlutterNativeTimezonePlugin(ObjC) | flutter_native_timezone | StandardMethodCodec |
| OHOS | FlutterNativeTimezonePlugin : FlutterPlugin, MethodCallHandler | flutter_native_timezone | 默认 StandardMethodCodec |
契约:通道名与方法名必须与 Dart 层完全一致,否则调用会落到
notImplemented()。
2.2 方法实现对照
| 方法 | Android | iOS | OHOS |
|---|---|---|---|
getLocalTimezone | ZoneId.systemDefault().id | NSTimeZone.localTimeZone.name | i18n.getTimeZone().getID() |
getAvailableTimezones | ZoneId.getAvailableZoneIds() | NSTimeZone.knownTimeZoneNames | i18n.TimeZone.getAvailableIDs() |
OHOS 核心实现(FlutterNativeTimezonePlugin.ets):
import{FlutterPlugin,FlutterPluginBinding,MethodCall,MethodCallHandler,MethodChannel,MethodResult,}from'@ohos/flutter_ohos';import{i18n}from'@kit.LocalizationKit';exportdefaultclassFlutterNativeTimezonePluginimplementsFlutterPlugin,MethodCallHandler{privatechannel:MethodChannel|null=null;getUniqueClassName():string{return"FlutterNativeTimezonePlugin"}onAttachedToEngine(binding:FlutterPluginBinding):void{this.channel=newMethodChannel(binding.getBinaryMessenger(),"flutter_native_timezone");this.channel.setMethodCallHandler(this)}onDetachedFromEngine(binding:FlutterPluginBinding):void{if(this.channel!=null){this.channel.setMethodCallHandler(null)this.channel=null}}onMethodCall(call:MethodCall,result:MethodResult):void{try{switch(call.method){case'getLocalTimezone':result.success(this.getLocalTimezone());break;case'getAvailableTimezones':result.success(this.getAvailableTimezones());break;default:result.notImplemented();}}catch(err){result.error("Error",`Failed to handle method${call.method}:${(errasError).message}`,null);}}// 获取本地时区,返回 IANA 时区 ID,如 Asia/ShanghaiprivategetLocalTimezone():string{consttimezone:i18n.TimeZone=i18n.getTimeZone();returntimezone.getID();}// 获取系统支持的可用 IANA 时区 ID 列表privategetAvailableTimezones():Array<string>{returni18n.TimeZone.getAvailableIDs();}}关键差异点:OHOS 用
@ohos/flutter_ohos的FlutterPlugin/MethodCallHandler接口替换 Android 的io.flutter.*;时区能力用@kit.LocalizationKit的i18n模块——i18n.getTimeZone()无参调用即返回系统当前时区的TimeZone对象(与 AndroidZoneId.systemDefault()、iOSlocalTimeZone语义一致),i18n.TimeZone.getAvailableIDs()返回系统支持的 IANA 时区 ID 列表。i18n相关 API 自 API 9 提供、@crossplatform语义,返回 ID 与 Android/iOS 同源。
2.3 选型决策:i18n vs systemDateTime
| 方案 | 优点 | 缺点 |
|---|---|---|
i18n.getTimeZone().getID()+i18n.TimeZone.getAvailableIDs()✅ | 一个模块同时覆盖"本地时区"与"可用列表"两个方法;getAvailableIDs是唯一能取全量列表的 API;ID 为 IANA 语义 | 依赖国际化模块(Kits 级,无额外权限) |
systemDateTime.getTimezoneSync() | 同步取值、调用更直接 | 只能取本地时区,无法提供可用时区列表,仍需第二个 API 来源 |
最终选择@kit.LocalizationKit的i18n方案,保证两个方法语义与 Android 一致。
第 3 阶段:三方库注册
原上游 pubspec 的 Dart SDK 约束为>=2.12.0 <3.0.0(Dart 2.x),与本机 Dart 3.11.5 冲突,需同步升级约束并在flutter.plugin.platforms中新增ohos:
environment:sdk:'>=3.4.0 <4.0.0'flutter:'>=3.22.0'flutter:plugin:platforms:android:package:com.whelksoft.flutter_native_timezonepluginClass:FlutterNativeTimezonePluginios:pluginClass:FlutterNativeTimezonePluginmacos:pluginClass:FlutterNativeTimezonePluginweb:pluginClass:FlutterNativeTimezonePluginfileName:flutter_native_timezone_web.dartohos:# ← 新增pluginClass:FlutterNativeTimezonePlugin# ← 与 index.ets 默认导出、getUniqueClassName() 一致说明:Flutter OHOS 引擎构建时会读取
pubspec.yaml的ohos配置,通过GeneratedPluginRegistrant自动加载ohos/index.ets导出的插件类,pluginClass必须与 ArkTS 类名、getUniqueClassName()返回值三者完全一致。
第 4 阶段:示例应用创建与构建
flutter create . --template=plugin会同步生成example/ohos/宿主工程(AppScope、entry 模块、ohosTest 测试模块、hvigor 配置等)。example 侧pubspec.yaml的 Dart SDK 约束同样需要升级。
构建签名 HAP(示例工程的签名配置由 DevEco Studio 自动签名注入本地调试证书):
cdexample flutter pub get flutter build hap--debug成功产出:
example/ohos/entry/build/default/outputs/default/entry-default-signed.hap踩坑:
flutter build hap首次会报「请通过 DevEco Studio 配置调试签名」——编译本身已通过(assembleHap成功),但 HAP 必须带签名。用 DevEco Studio 打开example/ohos开启自动签名后,signingConfigs会写入本地~/.ohos/config的调试证书,重跑即可产出 signed HAP。另外flutter pub get若命中pub.flutter-io.cn镜像网络异常,可临时PUB_HOSTED_URL=https://pub.dev重试。
第 5 阶段:真机验证与收尾
- 一致性代码检查(ohos-flutter-code-check):逐接口比对 Android/iOS/macOS/Web 与 OHOS 实现,两个公开接口均判定 ✅ 一致/基本一致,无需修复代码。
- 双语文档:按 flutter-library-document-optimization 规范生成
README.OpenHarmony_CN.md与README.OpenHarmony.md;兼容性信息中的 SDK 版本动态读取工程ohos/build-profile.json5(不硬编码),ROM 版本先标注未实测、真机验证后回填实测值。 - 真机验证:安装并运行 example,确认本地时区与可用时区列表展示正常(详见第六章环境与第七章截图)。
四、完整代码对照
4.1 Android vs OHOS 完整实现对照
| 维度 | Android (Kotlin) | OHOS (ArkTS) |
|---|---|---|
| 语言 | Kotlin / Java | ArkTS (TypeScript 语法) |
| 插件接口 | FlutterPlugin, MethodCallHandler(io.flutter.*) | FlutterPlugin, MethodCallHandler(@ohos/flutter_ohos) |
| 通道创建 | MethodChannel(messenger, "flutter_native_timezone") | new MethodChannel(binding.getBinaryMessenger(), "flutter_native_timezone") |
| 本地时区 | ZoneId.systemDefault().id | i18n.getTimeZone().getID() |
| 可用时区 | ZoneId.getAvailableZoneIds() | i18n.TimeZone.getAvailableIDs() |
| 生命周期 | onAttachedToEngine/onDetachedFromEngine | onAttachedToEngine/onDetachedFromEngine(额外置空 channel) |
| 未知方法 | result.notImplemented() | result.notImplemented()(外层 try/catch 兜底) |
| 兼容 v1 注册 | companion object registerWith(Registrar) | 无需(OHOS 引擎统一走插件注册表) |
4.2 关键 ArkTS 语法差异
| Android 语法 | ArkTS 语法 | 备注 |
|---|---|---|
import io.flutter.plugin.common.* | import { FlutterPlugin, MethodChannel, ... } from '@ohos/flutter_ohos' | OHOS 使用显式具名导入 |
import java.time.ZoneId | import { i18n } from '@kit.LocalizationKit' | 时区/区域能力来自 Kit 化模块 |
binding.binaryMessenger | binding.getBinaryMessenger() | OHOS 为方法调用 |
类成员可空lateinit | private channel: MethodChannel | null = null | ArkTS 显式联合类型 + null 判空 |
| 无接口方法级 throws | try { ... } catch (err) { result.error(...) } | ArkTS 对平台调用建议统一兜底 |
五、关键决策说明
决策 1:以官方上游 pinkfish master 为干净基线(reset)
本仓库此前基于另一个 flutter_timezone 分支谱系做过一次适配,历史混杂。为产出可长期跟随官方上游的 0-1 适配,直接git reset --hard upstream/master(fcc1f3f,2.0.1)重建基线,再叠加本次 OHOS 适配提交;旧分支保留、默认分支切换为main。
维护策略:后续跟随pinkfish/flutter_native_timezonemaster 拉取更新,OHOS 实现单独演进。
决策 2:保持通道名与方法名完全不变
Dart 层MethodChannel('flutter_native_timezone')、方法getLocalTimezone/getAvailableTimezones是 Dart 与原生之间的通信契约,OHOS 侧原样沿用,保证Dart 层零改动。
维护策略:新增方法时三端(Dart / Android / OHOS)同步注册。
决策 3:时区能力统一走@kit.LocalizationKit的i18n
对比后采用i18n.getTimeZone().getID()与i18n.TimeZone.getAvailableIDs()同时覆盖两个方法,ID 语义为 IANA(与 Android/iOS 同源),避免systemDateTime只能取单值的缺口。
维护策略:若 SDK 后续提供更全量的时区表 API,可平滑替换getAvailableTimezones内部实现。
决策 4:SDK / 版本信息动态读取,不硬编码
兼容性与文档中的 SDK 版本一律读取工程ohos/build-profile.json5的compatibleSdkVersion/targetSdkVersion(或本机~/Library/OpenHarmony/Sdk/<version>/目录名),ROM 版本以真机hdc shell param get实测值回填,未实测前明确标注"未实测",杜绝文档与环境脱节。
维护策略:升级 SDK 或真机环境变更时,文档同步按实测值更新,不沿用旧版本示例。
决策 5:错误处理采用 try/catch 兜底
OHOS 原生方法统一包 try/catch,异常时通过result.error回传而非静默,避免 Dart 层收到异常类型崩溃,同时保持与 Android/iOS 的返回结构一致。
维护策略:后续方法扩展沿用同一错误回传格式。
六、测试与验证
测试环境
| 项目 | 版本 |
|---|---|
| Flutter | 3.41.10-ohos-1.0.0(channel [user-branch]) |
| Dart | 3.11.5 |
| HarmonyOS SDK | compatibleSdkVersion 5.1.0(18) / targetSdkVersion 26.0.0(读取自ohos/build-profile.json5;DevEco 默认 SDK 26.0.0.105) |
| IDE | DevEco Studio 26.0.0 |
| 设备 ROM | OpenHarmony-7.0.0.105(API 26,真机 ALN-AL00,const.ohos.fullname实测) |
版本获取方式:
| 版本项 | 获取方式 |
|---|---|
| Flutter / Dart | flutter --version |
| HarmonyOS SDK | 读取example/ohos/build-profile.json5的compatibleSdkVersion/targetSdkVersion(或~/Library/OpenHarmony/Sdk/<version>/目录名) |
| IDE | /usr/libexec/PlistBuddy -c "Print :CFBundleShortVersionString" /Applications/DevEco-Studio.app/Contents/Info.plist |
| 设备 ROM | hdc shell param get const.ohos.fullname/hdc shell param get const.ohos.apiversion(先hdc list targets确认设备连接) |
验证要点
- 本地时区获取— 真机运行 example,
getLocalTimezone()返回Asia/Shanghai,与设备设置一致 - 可用时区列表—
getAvailableTimezones()返回数百个 IANA 时区 ID 并正常渲染列表,sort()后有序展示 - 通道一致性— 未知方法返回
notImplemented(),原生异常经result.error回传不崩溃 - 一致性代码检查— ohos-flutter-code-check 逐接口比对 Android/iOS/macOS/Web 与 OHOS:两个公开接口均 ✅ 一致/基本一致,无代码修复项
- 构建验证—
flutter build hap --debug成功产出entry-default-signed.hap - 插件注册— example 的
.flutter-plugins-dependencies中flutter_native_timezone已含 ohos 平台与native_build: true
七、运行效果
真机(ALN-AL00,OpenHarmony-7.0.0.105)运行 example,示例页展示本地时区与可用时区列表。截图通过hdc shell snapshot_display抓取:
hdc shell snapshot_display-f/data/local/tmp/shot.jpeg# 注意:后缀必须为 .jpeghdcfilerecv /data/local/tmp/shot.jpeg ./ohos_run_timezone.jpeg八、遗留问题与改进方向
踩坑复盘
| 踩坑点 | 现象 / 报错 | 根因与解法 |
|---|---|---|
| 上游 SDK 约束过老 | flutter pub get解析失败 | 上游 pubspecsdk: '>=2.12.0 <3.0.0'与 Dart 3.11 冲突;升级为>=3.4.0 <4.0.0、flutter >=3.22.0 |
flutter create组织名歧义 | Ambiguous organization in existing files: {com.whelksoft, com.example} | 显式指定--org com.whelksoft |
| 模板文件溢出 | 生成 Gradle kts、federated 骨架、iOS Swift 副本等非目标文件 | 逐项甄别删除,仅保留ohos/、example/ohos/与 pubspec 变更 |
| pub 镜像网络异常 | Got socket error ... pub.flutter-io.cn | 临时PUB_HOSTED_URL=https://pub.dev重试 |
| 缺少调试签名 | 请通过DevEco Studio打开ohos工程后配置调试签名 | 编译已通过但 HAP 未签名;DevEco Studio 自动签名注入~/.ohos/config调试证书后重跑 |
| 截图命令报错 | fileName /data/local/tmp/x.png invalid, suffix must be .jpeg | snapshot_display仅支持.jpeg后缀 |
| 目标仓库已存在 | 仓库创建报 422 Project already exists | 复用已存在的oh-flutter/flutter_native_timezone(旧分支保留),推送main并切换默认分支 |
已知问题
- Web 平台可用时区列表退化— 上游 Web 实现受
Intl限制,getAvailableTimezones()仅返回本地时区单元素(上游既有行为,与本次 OHOS 适配无关) - OHOS 可用时区为系统裁剪集合—
i18n.TimeZone.getAvailableIDs()返回系统支持的集合,个别极冷门 IANA ID 可能不在其中,常规场景不受影响
未来优化
- 补充 ohosTest— 为 example 的 ohos 测试模块补时区接口用例,纳入自动化回归
- CI 集成— 在
.github/workflows中增加 ohos 平台的构建校验,防止后续改动破坏 OHOS 编译 - 跟随上游— 定期同步
pinkfish/flutter_native_timezone上游变更,保持 Dart/Android/iOS 代码不落后
九、总结
将一个 Flutter 三方库适配到 OHOS 平台,核心路径可以概括为三步走:
1. 找对应 ── 找到 OHOS 对每个 Android 原生 API 的等价实现(ZoneId → i18n.TimeZone) 2. 保契约 ── 确保方法通道名、方法名、返回值结构完全一致(Dart 层零改动) 3. 补缺口 ── 对于 OHOS 不提供的 API,用合理方案弥补并如实记录差异对于flutter_native_timezone三方库,适配新增ohos/(7 个文件)与example/ohos/(30+ 个文件)两个平台目录,pubspec.yaml仅增加 2 行ohos注册并升级环境约束。Dart 层与其他平台的代码完全不受影响——这正是 Flutter 跨平台三方库生态的魅力所在。
参考文档
- flutter_native_timezone 官方仓库(pinkfish)
- 本仓库 AtomGit 镜像(oh-flutter/flutter_native_timezone)
- HarmonyOS Flutter 适配指南(CPF-Flutter/flutter_flutter)
- OpenHarmony i18n 国际化模块 API 参考
- ohos-flutter-plugin-adapter skill(CPF-Flutter/skills)