鸿蒙Flutter 三方库 country_codes 的 适配实战

本文记录了将开源 Flutter 插件 country_codes 适配到 OpenHarmony / HarmonyOS NEXT 平台的完整过程,包含适配思路、代码改动对照、关键决策和踩坑复盘。


一、背景

1.1 插件简介

country_codes 是一个 Flutter 社区广泛使用的国家代码辅助插件,提供以下能力:

  • 获取设备当前语言和国家(如 zh / CN
  • 获取本地化的国家名称(如在英文环境下显示 “China”,中文环境下显示 “中国”)
  • ISO 3166-1 双字母/三字母代码查询
  • 国际电话区号查询

该插件最初支持 Android、iOS、macOS 三个平台,本次任务将其适配到 OpenHarmony / HarmonyOS NEXT 平台。

项目地址https://gitcode.com/oh-flutter/country_codes

1.2 适配目标

维度 要求
功能一致性 三个原生方法(getLocale / getRegion / getLanguage)返回的数据结构与 Android 完全一致
Dart 层零改动 现有 Dart API 无需修改即可在 OHOS 上工作
性能 本地化国家名称查询需完整覆盖所有 ISO 3166-1 国家
工程规范 遵循 Flutter OHOS 插件标准目录结构

二、适配路线图

整个适配分为 4 个阶段:

第 1 阶段:项目初始化 ── 创建 ohos/ 目录,配置标准模板
第 2 阶段:原生实现   ── 编写 CountryCodesPlugin.ets,实现三个原生方法
第 3 阶段:插件注册   ── 在 pubspec.yaml 中注册 ohos 平台
第 4 阶段:示例验证   ── 创建 example/ohos/ 验证端到端流程

三、逐步适配过程

第 1 阶段:项目初始化

使用 Flutter 命令行生成 OHOS 模板
flutter create . --template=plugin --platforms=ohos

该命令会自动生成 ohos/ 目录的标准模板结构,包含必要的构建配置和入口文件。开发者只需关注核心插件逻辑的实现。

创建的目录结构
ohos/
├── index.ets                              # 模块入口,导出插件类
├── oh-package.json5                       # 包配置
├── build-profile.json5                    # 构建配置
├── src/main/
│   ├── module.json5                       # HAR 模块配置
│   └── ets/components/plugin/
│       └── CountryCodesPlugin.ets         # 原生插件实现(核心)

关键文件说明:

  • oh-package.json5 — 声明包名、入口文件、许可证和依赖
  • module.json5 — 声明模块类型为 har(静态共享库),支持设备类型
  • build-profile.json5 — 配置构建目标
  • index.ets — 模块导出入口
index.ets(入口导出文件)
import CountryCodesPlugin from './src/main/ets/components/plugin/CountryCodesPlugin';
export default CountryCodesPlugin;
oh-package.json5
{
  "name": "country_codes",
  "version": "1.0.0",
  "main": "index.ets",
  "license": "Apache-2.0",
  "dependencies": {}
}

@ohos/flutter_ohos 由 Flutter 引擎在构建时自动链接,无需在 dependencies 中显式声明。

module.json5
{
  "module": {
    "name": "country_codes",
    "type": "har",
    "deviceTypes": ["default", "tablet"]
  }
}

第 2 阶段:原生实现(核心)

这是适配的核心工作。我们将 Android 平台的 Kotlin 实现逐一翻译为 ArkTS。

2.1 整体架构对比
 Android (Kotlin)                          OHOS (ArkTS)
 ────────────────────                      ────────────────────
 class CountryCodesPlugin                   class CountryCodesPlugin
   implements FlutterPlugin,                  implements FlutterPlugin,
              MethodCallHandler                             MethodCallHandler
                                               import { FlutterPlugin,
   import io.flutter.embedding.engine.                    FlutterPluginBinding,
     plugins.FlutterPlugin                               MethodCall,
   import io.flutter.plugin.common.                      MethodCallHandler,
     MethodChannel                                        MethodChannel,
   import java.util.Locale                                MethodResult
                                               } from '@ohos/flutter_ohos'
                                               import i18n from '@ohos.i18n'
2.2 方法通道注册
平台 代码
Android MethodChannel(flutterPluginBinding.getFlutterEngine().getDartExecutor(), "country_codes")
OHOS new MethodChannel(binding.getBinaryMessenger(), "country_codes")

差异:OHOS 使用 getBinaryMessenger() 替代 Android 的 getFlutterEngine().getDartExecutor(),接口更简洁。

2.3 三个原生方法实现对照
方法 getLanguage — 获取系统语言
平台 实现 返回值
Android Locale.getDefault().language "zh", "en", etc.
OHOS i18n.System.getSystemLanguage() "zh", "en", etc.
方法 getRegion — 获取系统地区
平台 实现 返回值
Android Locale.getDefault().country "CN", "US", etc.
OHOS i18n.System.getSystemRegion() "CN", "US", etc.
方法 getLocale — 获取完整语言环境 + 本地化名称

这是最复杂的方法,需要返回 [语言, 地区, 本地化国家名称映射] 三元组。

Android 实现(Kotlin):

result.success(
  listOf(
    Locale.getDefault().language,
    Locale.getDefault().country,
    getLocalizedCountryNames(call.arguments as String?)
  )
)

OHOS 实现(ArkTS):

const language = i18n.System.getSystemLanguage();
const region = i18n.System.getSystemRegion();
const localeTag = call.args as string;
const localizedCountries = this.getLocalizedCountryNames(localeTag);
result.success([language, region, localizedCountries]);

关键差异点:Android 使用 call.arguments(带 s),而 ArkTS 使用 call.args(不带 s)。

2.4 本地化国家名称的实现差异

这是适配中最值得展开的部分。

Android 的做法:

// 1. 利用 Java 标准库获取所有 ISO 国家代码
for (countryCode in Locale.getISOCountries()) {
    // 2. 为目标国家代码创建 Locale 对象
    val locale = Locale(localeTag ?: deviceCountry, countryCode)
    // 3. 获取本地化显示名称
    var countryName = locale.getDisplayCountry(
        Locale.forLanguageTag(localeTag ?: deviceCountry)
    )
    localizedCountries[countryCode.toUpperCase()] = countryName ?: ""
}

OHOS 的做法:

// 1. OHOS 没有类似 Locale.getISOCountries() 的系统 API
//    → 解决方案:硬编码完整 ISO 3166-1 国家代码列表(249 个)
const countryCodes = this.getISOCountryCodes();

for (const countryCode of countryCodes) {
    // 2. 使用 @ohos.i18n 的 getDisplayCountry API
    const countryName = i18n.getDisplayCountry(
        countryCode, targetLocale, false
    );
    localizedCountries[countryCode.toUpperCase()] = countryName ?? "";
}
2.5 硬编码国家代码列表

OHOS 的 @ohos.i18n 提供了语言、地区获取和本地化名称显示的能力,但 没有提供获取所有 ISO 国家代码列表的系统 API(类似于 Java 的 Locale.getISOCountries())。

决策:硬编码 vs 动态获取

方案 优点 缺点
硬编码 249 个 ISO 代码 实现简单,与 Android 数据一致,无运行时开销 需维护列表,新增国家需更新
动态从 i18n API 获取 未来扩展性好 OHOS 不提供该 API

最终选择硬编码方案,直接在 CountryCodesPlugin.ets 中以数组形式列出全部 249 个 ISO 3166-1 alpha-2 代码,确保与 Android 端 Locale.getISOCountries() 返回的数据一致。


第 3 阶段:插件注册

pubspec.yaml 中添加 OHOS 平台注册:

flutter:
  plugin:
    platforms:
      android:
        package: com.miguelruivo.flutter.plugin.countrycodes.country_codes
        pluginClass: CountryCodesPlugin
      ios:
        pluginClass: CountryCodesPlugin
      macos:
        pluginClass: CountryCodesPlugin
      ohos:                              # ← 新增
        pluginClass: CountryCodesPlugin  # ← 对应 index.ets 默认导出

Flutter 的 OHOS 引擎在构建时会读取 pubspec.yaml 中的 ohos 配置,自动加载 ohos/index.ets 中导出的插件类。

第 4 阶段:示例应用创建

创建 example/ohos/ 目录用于端到端测试:

example/ohos/
├── AppScope/app.json5                     # 应用配置
├── build-profile.json5                   # 项目构建配置(含 SDK 版本)
├── hvigor/hvigor-config.json5            # 构建工具配置
├── oh-package.json5                      # 顶层包配置
├── hvigorfile.ts                         # 构建入口
├── entry/
│   ├── build-profile.json5
│   ├── oh-package.json5
│   ├── src/main/
│   │   ├── module.json5                  # entry 模块配置
│   │   ├── ets/
│   │   │   ├── entryability/
│   │   │   │   └── EntryAbility.ets      # Ability 生命周期
│   │   │   └── pages/
│   │   │       └── Index.ets             # UI 页面(Flutter 容器)
│   │   └── resources/
│   └── src/ohosTest/                     # 测试目录

四、完整代码对照

4.1 Android vs OHOS 完整实现对照

维度 Android (Kotlin) OHOS (ArkTS)
语言 Kotlin / Java ArkTS (TypeScript 语法)
系统语言 API Locale.getDefault().language i18n.System.getSystemLanguage()
系统地区 API Locale.getDefault().country i18n.System.getSystemRegion()
本地化国家名 Locale.getDisplayCountry() i18n.getDisplayCountry()
ISO 国家列表 Locale.getISOCountries() 硬编码 249 个 alpha-2 代码
方法通道名 "country_codes" "country_codes"(保持一致)
参数获取 call.arguments call.args
返回值格式 listOf(lang, region, map) [lang, region, map]

4.2 关键 ArkTS 语法差异

Android 语法 ArkTS 语法 备注
import io.flutter... import { ... } from '@ohos/flutter_ohos' OHOS 使用模块化导入
Locale.getDefault() i18n.System.getSystemXxx() API 命名不同
HashMap Record<string, string> 类型系统差异
call.arguments call.args 属性名不同
result.success(listOf(...)) result.success([...]) 语法差异

五、关键决策说明

决策 1:保持通道名不变 — "country_codes"

Dart 层 MethodChannel('country_codes') 已经固定,OHOS 原生侧必须使用完全相同的通道名。这是跨平台插件适配的铁律——通道名是 Dart 与原生之间的通信契约

决策 2:Dart 层零改动

country_codes.dartMethodChannel.invokeMethod('getLocale', ...) 调用在 OHOS 上无需任何修改,因为:

  • 方法签名 invokeMethod('getLocale', localeTag) 完全不变
  • 返回的数据结构 [String, String, Map] 与 Android 一致
  • 异常处理逻辑(失败返回 ["", "", {}])也一致

决策 3:硬编码 ISO 国家列表

原因:OHOS 的 @ohos.i18n 基于 Unicode CLDR 提供了本地化能力,但没有暴露获取所有国家代码列表的系统 API。硬编码是当前最务实的方案。

维护策略:列表直接从 Android 的 Locale.getISOCountries() 输出中提取,以后跟随 Android SDK 更新即可。

决策 4:错误处理采用静默降级

private handleGetRegion(result: MethodResult): void {
    try {
        const region = i18n.System.getSystemRegion();
        result.success(region);
    } catch (err) {
        result.success("");  // ← 静默降级,返回空字符串
    }
}

所有原生方法在异常时返回空值而非抛出异常,保证 Dart 层不因原生异常而崩溃。


六、测试与验证

测试环境

项目 版本
Flutter 3.41.10-ohos-0.0.1-canary1
Dart 3.11.5
HarmonyOS SDK 5.1.0(18)
IDE DevEco Studio 6.1.0
设备 ROM ALN-AL00 6.1.0.117(SP6C00E115R4P9)

验证要点

  1. 初始化await CountryCodes.init() 返回 true
  2. 设备 LocaleCountryCodes.getDeviceLocale() 返回 Locale('zh', 'CN')
  3. 国家详情CountryCodes.detailsForLocale() 返回正确的国家信息
  4. 所有国家列表CountryCodes.countryCodes() 返回 249 条记录
  5. 本地化名称 — 传入 en 返回英文名,传入 zh 返回中文名

七、运行效果

适配完成后,通过 flutter screenshot 命令获取 OpenHarmony 设备上的运行截图:

flutter screenshot -d 192.168.10.251:42923

运行截图


八、遗留问题与改进方向

已知问题

  1. 静态国家代码列表 — 硬编码列表不随系统更新自动增加新国家代码
  2. 参数类型安全 — 当前 call.args as string 直接强转,传入非字符串参数会抛出异常

未来优化

  • 从系统 API 动态发现国家代码 — 待 OHOS SDK 提供类似 Locale.getISOCountries() 的 API 后可替换
  • 参数校验 — 增加 typeof call.args === 'string' 类型检查
  • 单元测试 — 编写 OHOS 平台插件的自动化测试用例

八、总结

将一个 Flutter 插件适配到 OHOS 平台,核心路径可以概括为 三步走

1. 找对应 ── 找到 OHOS 对每个 Android 原生 API 的等价实现
2. 保契约 ── 确保方法通道名、方法名、返回值结构完全一致
3. 补缺口 ── 对于 OHOS 不提供的 API(如 getISOCountries),用合理方案弥补

对于 country_codes 插件,适配涉及两个文件的新增(CountryCodesPlugin.ets + index.ets)和一行 pubspec.yaml 的修改。Dart 层和其他平台的代码完全不受影响——这正是 Flutter 跨平台插件生态的魅力所在。


参考文档

Logo

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

更多推荐