开发工具: 华为云码道

原始仓库: abom-me/app_local

本文配套仓库: oh-flutter/fluttertpc_app_local

app_local 将基于 JSON 文件的应用内多语言能力封装为 Flutter 插件,应用可以加载多语言资源、跟随手机语言设置默认语言,并在运行时直接切换语言而无需重启。本文以 app_local 1.1.3 为例,介绍源码准备、OHOS 工程配置、ArkTS 实现和 example 真机运行。

在这里插入图片描述

插件目前支持 ohos 平台,源码位于 AtomGit 配套仓库。文中的代码以上游提交 0ce28ab525d24199459bff0d8fdc7eafee851192(1.1.3)为适配基线,OHOS 相关改动包括 Dart 层的平台分支、ohos/ 平台实现和 example/ohos/ 宿主工程。

在这里插入图片描述

一、插件简介与适配目标

应用内多语言是出海与多地区应用的基础能力。应用把各语言的文案放在 JSON 文件里,由库按当前语言加载并替换文案,业务层不需要自己解析资源文件,也不需要为切换语言重启应用。

示例首页(英文) 已加载的本地化键列表 语言设置页 示例首页(英文) 示例首页(英文) 示例首页(英文) 示例首页(英文) 示例首页(英文) 示例首页(英文) 示例首页(英文) 示例首页(英文) 示例首页(英文)

例如,阅读器可以跟随系统语言选择默认文案,工具类应用可以在设置页提供"English / العربية"切换,表单类应用也可以在文案里用占位符填入用户昵称等动态参数。

语言检测通过平台通道完成:Locales.init 初始化时调用 AppLang().getPhoneLanguage() 获取手机语言标签,作为首次启动的默认语言;语言偏好的持久化交给 shared_preferences。

因此本库的鸿蒙化要做三件事:在 pubspec.yaml 补齐 ohos 平台声明;在 Dart 侧补上 Platform.isOhos 分支,让通道在 OHOS 上真正被调用;在 ArkTS 侧实现与 Android/iOS 语义一致的 getPhoneLanguage。


二、环境准备

环境搭建参考社区文档: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-devFlutter OHOS 对应开发分支
Dart SDK3.13.3Dart 语言与包管理环境
HarmonyOS 开发套件26.0.0 release(26.0.0.851)本机 DevEco Studio 与安装的 SDK 版本(API 26)
compileSdkVersion未在 product 中显式声明编译时使用本机 DevEco 安装的 SDK
targetSdkVersion6.1.1(24)应用面向的行为版本
compatibleSdkVersion6.1.1(24)当前工程声明的最低兼容版本
插件版本1.1.3pubspec.yaml 中的包版本
原生语言ArkTSHarmonyOS 插件实现
插件产物HAR被应用 entry 模块依赖

2.1 开发套件版本与工程中的 SDK 版本配置

本机 DevEco Studio 为 26.0.0,对应 API 26;宿主工程却把 compatibleSdkVersion 和 targetSdkVersion 声明为 6.1.1(24)。两者不是同一套编号,含义如下:

  • 26.0.0(API 26) 表示本机安装的 HarmonyOS 开发套件版本为 26.0.0,对应 API 26,编译时实际使用的就是它。
  • 6.1.1(24) 是本文 HarmonyOS 应用工程中 compatibleSdkVersion 和 targetSdkVersion 的属性值,表示 HarmonyOS 6.1.1,对应 API 24。

对应的 product 配置为:

{
  "name": "default",
  "signingConfig": "default",
  "compatibleSdkVersion": "6.1.1(24)",
  "targetSdkVersion": "6.1.1(24)",
  "runtimeOS": "HarmonyOS"
}

在这里插入图片描述

这组配置以 API 24 为目标版本,并以 API 24 作为最低兼容版本,与本文测试设备的 OpenHarmony-6.1.1.120(API 24)一致。打包信息 pack.info 中 apiVersion 为 {compatible: 24, target: 24, releaseType: "Release"}。DevEco 只安装了 API 26 的 SDK 并不影响该写法:compatibleSdkVersion 声明的是安装与运行的门槛,编译仍然使用已安装的 SDK。

版本号写法有一条固定规则:API ≥ 26 时直接写纯版本号(如 26.0.0),API < 26 必须写成 版本(API) 形式(如 6.1.1(24))。API 与版本号的对应关系可查 DevEco 的 hos-config.json(osVersionMapper)。安装报版本不匹配时,按这条规则对齐,而不是盲目沿用模板生成的 SDK 版本号。


三、从源码仓库开始准备适配工程

3.1 将上游源码同步到 AtomGit

适配已有第三方插件时,先从 Pub 包信息或项目 README 确认上游地址、包名、版本和许可证。同步仓库时保留源码、许可证和提交历史,方便后续跟踪上游更新。

在 AtomGit 网页的新建仓库流程中,找到从已有仓库导入的入口,填写上游 Git 地址,选择自己有写权限的组织或个人空间,设置目标仓库名称后执行导入。完成后检查目标分支、pubspec.yaml、LICENSE 和提交历史是否完整,并记录适配起点的提交号。如果上游本身托管在 AtomGit,也可以通过 Fork 获得自己的工作仓库。

app_local 已从上游 abom-me/app_local 同步到 AtomGit。上游包名为 app_local,版本为 1.1.3,许可证为 MIT。下面使用 AtomGit 地址拉取代码;需要提交修改时,使用自己有写权限的仓库或 Fork。

3.2 将代码拉取到宿主机

在安装了 Flutter OH 和 DevEco Studio 的开发电脑上打开终端,进入准备存放项目的目录,执行:

git clone https://atomgit.com/oh-flutter/fluttertpc_app_local.git
cd fluttertpc_app_local
pwd
ls
git remote -v
git status --short --branch
git rev-parse HEAD

请添加图片描述

git clone 会创建 fluttertpc_app_local/ 目录;cd 后的位置就是下文所说的插件仓库根目录,这里应能看到 pubspec.yaml、lib/ 和 example/。Git 仓库名是 fluttertpc_app_local,Dart 包名是 app_local。

需要使用与本文相同的适配基线时,在没有未提交修改的仓库中切换到以下提交:

git switch --detach 0ce28ab525d24199459bff0d8fdc7eafee851192

本文的 OHOS 改动在该基线之上完成。核对基线与当前改动的状态:

git log --oneline -3
git status --short --branch

请添加图片描述

适配其他插件时,使用该插件对应版本的 tag 或 commit 作为分支起点。

3.3 在仓库根目录确定适配分支与版本标记

本库的适配提交落在 main 上,版本通过 TAG 标记。TAG 命名规则为 原库版本-ohos-版本号-beta.x,本库使用 1.1.3-ohos-1.0.0-beta.1。在 fluttertpc_app_local/ 根目录执行:

git switch main
git branch -a
git log --oneline -3

git branch -a 用于确认当前所在分支与远程分支。向接收仓库提交时按其对分支与 TAG 的要求执行;需要在自己的 Fork 上独立维护时,再新建 feat/ohos_app_local_1.1.3 之类的分支也不影响上述规则。注意保持文档一致:README.OpenHarmony_CN.md 的 TAG 版本对应表中"分支"一列当前写的是 br_v1.1.3_ohos,与仓库实际的 main 不同,发布前需要统一口径。

3.4 自动补全 OHOS 适配结构

确认分支后,仍在同一个插件根目录执行结构补全。以下命令适用于尚无 ohos/ 目录的既有 Flutter 平台插件:

flutter create --template=plugin --platforms=ohos --project-name app_local .
git status --short
git diff -- pubspec.yaml lib example

请添加图片描述

  • --template=plugin 指定插件模板。
  • --platforms=ohos 指定需要补全的平台。
  • --project-name app_local 使用 Dart 包名,避免把仓库目录名作为包名。
  • 最后的 . 表示在当前插件目录补全工程,不是另建一层 app_local/。

补全 OHOS 结构的命令

图 3:补全 OHOS 结构的命令(本例仓库已包含 `ohos/`,无需重复执行)

该命令生成 OHOS 平台脚手架。生成后务必用 diff 检查:模板会连带注入与 OHOS 无关的内容,例如 federated 骨架、.kts 构建脚本、*.iml、local.properties,还可能改写 analysis_options.yaml。本例保留了上游已有的 API、其他平台注册项及依赖配置,只新增 ohos/ 与 example/ohos/。不同 Flutter OH 版本生成的模板可能略有差异。

如果生成后 example/ohos/ 仍不存在,进入已有示例应用补全平台:

cd example
flutter create --platforms=ohos .
cd ..

配套仓库已经包含 ohos/ 和 example/ohos/,直接运行示例时可以跳过结构补全。

3.5 适配后的项目目录

适配后的关键目录如下:

fluttertpc_app_local/
├── lib/
│   ├── app_local.dart
│   ├── plug/
│   │   ├── app_lang.dart
│   │   ├── app_lang_platform_interface.dart
│   │   └── app_lang_method_channel.dart
│   └── src/
│       └── locales.dart 等本地化实现
├── ohos/
│   ├── index.ets
│   ├── oh-package.json5
│   ├── build-profile.json5
│   └── src/main/
│       ├── ets/components/plugin/AppLangPlugin.ets
│       └── module.json5
├── example/
│   ├── lib/main.dart
│   ├── assets/a/
│   └── ohos/entry/
├── test/
├── README.OpenHarmony_CN.md
├── CHANGELOG.OpenHarmony.md
└── pubspec.yaml

请添加图片描述

项目根目录如下,其中包含 ohos/、example/,以及 OpenHarmony 中英文说明和变更记录文件:

文件主要职责
lib/app_local.dart对外导出入口,暴露 Locales、LocaleText 等能力
lib/plug/app_lang_method_channel.dart实现 app_lang 通道调用与平台判断
lib/src/locales.dart初始化、加载 JSON 资源、切换语言与查询
AppLangPlugin.ets注册 Flutter 通道并返回系统语言标签
插件 module.json5声明 HAR 模块与设备类型,本库不申请权限
示例 entry module.json5声明宿主应用 Ability、设备类型与权限场景
example/lib/main.dart展示初始化、语言切换、RTL 判断和键值列表
example/assets/a/英文与阿拉伯文的 JSON 文案资源

四、Dart 接口与通道分析

OHOS 实现需要遵循 Dart 层已有的方法、参数和回调约定。先阅读 lib/plug/app_lang_platform_interface.dart、lib/plug/app_lang_method_channel.dart 和 lib/src/locales.dart,再在 ohos/src/main/ets/components/plugin/AppLangPlugin.ets 中实现对应的原生调用。适配其他已有平台的插件时,也应保留其公开接口和其他平台实现。

本例的对应关系如下:

Dart 入口或模型通道协议OHOS 实现应保持的行为
AppLang().getPhoneLanguage()app_lang / getPhoneLanguagei18n.System.getSystemLanguage()返回 BCP-47 语言标签,如 zh-Hans、en
语言码提取无,纯 Dart 处理不涉及原生代码取标签第一段,如 zh-Hans-CN 取 zh
不支持的平台/异常无不涉及原生代码返回兜底值 en-OM,由业务回落到受支持语言

原生端需要保持通道名、方法名和返回值结构与其余平台一致。方法执行异常时,通过错误回调通知 Dart 层。

4.1 跨端架构与调用时序

Flutter 侧和 HarmonyOS 侧之间只有一条平台通道,语言切换和文案加载都在 Dart 层完成:

  1. MethodChannel('app_lang'):只在初始化时调用一次 getPhoneLanguage,获取手机语言;
  2. 语言切换与 JSON 加载:Locales 在 Dart 层完成,shared_preferences 负责记住用户选择。

Flutter 页面

Locales.init 初始化

AppLang.getPhoneLanguage

AppLangPlatform 平台接口

MethodChannel app_lang

ArkTS AppLangPlugin

i18n.System.getSystemLanguage

rootBundle 加载 JSON 文案

shared_preferences 语言偏好

MethodChannel 只服务于"手机语言是什么"这一个查询;用户切换语言、重建界面、加载文案都发生在 Dart 侧,与平台无关。

4.1.1 一次初始化与一次语言切换的时序
i18n.System AppLangPlugin.ets Locales / PreferenceUtils Flutter App i18n.System AppLangPlugin.ets Locales / PreferenceUtils Flutter App Locales.init(localeNames, localPath) invokeMethod(getPhoneLanguage) getSystemLanguage() zh-Hans result.success("zh-Hans") 取第一段 zh,匹配 supportedLocales 加载 JSON 并进入首页 context.changeLocale('ar') setLocale 写入偏好并通知重建 LocaleBuilder 重建子树,文案即时切换

getPhoneLanguage 只在 Locales.init 和 PreferenceUtils.init 中被调用;切换语言不再经过通道。

4.2 语言标签模型:BCP-47 标签与语言码提取

原生 API 返回 BCP-47 语言标签,Dart 层只取第一段作为语言码:

deviceLocale =
    (await AppLang().getPhoneLanguage() ?? "en-OM").split("-").first;
原生返回提取结果业务含义
zh-Hanszh简体中文设备
zh-Hans-CNzh带地区的中文标签,取段后等价
en-USen英文设备
null 或异常en-OM 兜底,再取 en通道不可用时的默认值

拿到语言码后,_determineInitialLocale 在 supportedLocales 中匹配:设备语言受支持时使用它,否则回落到列表第一个语言。因此返回 zh-Hans 还是 zh-CN,对上层行为完全等价——这也是 ArkTS 侧可以只返回 getSystemLanguage() 的原因。

4.3 公开 API 与平台接口

AppLangPlatform 定义平台接口,业务通过 AppLang 调用它。本地化能力则全部由 Locales 静态方法和组件承担,主要入口如下:

await Locales.init(
  localeNames: ['en', 'ar'],
  localPath: "assets/a/",
  // phoneLocale: true // 首次启动时跟随手机语言
);
名称说明类型ohos 支持
Locales.init初始化本地化配置functionyes
Locales.string按键取本地化文本,支持占位符参数functionyes
Locales.change切换当前语言functionyes
Locales.currentLocale / Locales.lang获取当前语言function / propertyyes
Locales.isDirectionRTL判断是否从右向左排版functionyes
LocaleBuilder语言变化时重建子树widgetyes
LocaleText本地化文本组件widgetyes
context.changeLocale / String.localizeBuildContext 与 String 扩展extensionyes
AppLang.getPhoneLanguage获取手机语言标签functionyes

除 getPhoneLanguage 外,其余接口不经过平台通道,在 OHOS 上与 Android、iOS 行为一致,无需任何平台分支。

4.4 Dart 通道协议分析

4.4.1 通道名称必须两端完全一致

final methodChannel = const MethodChannel('app_lang');

通道名称属于跨语言协议。注意通道名是 app_lang 而不是包名 app_local——Dart 和 ArkTS 任何一端拼写不一致,都会出现"方法未实现"或永远走兜底值的问题。

4.4.2 订阅实现:补上 Platform.isOhos 分支

这是本库 Dart 层唯一的适配改动,也是最容易踩的坑。上游实现按平台判断是否调用通道:

// 上游(OHOS 上会静默失效)
if (Platform.isAndroid || Platform.isIOS || Platform.isMacOS) {
  final version =
      await methodChannel.invokeMethod<String>('getPhoneLanguage');
  return version;
}
return "en-OM";

Platform.isOhos 是 Flutter-OHOS 引擎提供的平台判断。缺少该分支时,OHOS 上条件不成立,直接落到 return "en-OM" 兜底,原生通道永远不会被调用,所有设备都会被当成英文。适配后的实现补上了这一分支:


Future<String?> getPhoneLanguage() async {
  if (kIsWeb) {
    return "en-OM";
  }

  try {
    // Platform.isOhos 是 Flutter-OHOS 引擎提供的平台判断,
    // 缺少该分支时 OHOS 会直接落到下面的 "en-OM" 兜底。
    if (Platform.isAndroid ||
        Platform.isIOS ||
        Platform.isMacOS ||
        Platform.isOhos) {
      final version =
          await methodChannel.invokeMethod<String>('getPhoneLanguage');
      return version;
    }
    return "en-OM";
  } catch (e) {
    return "en-OM";
  }
}

任何异常(包括插件未注册抛出的 MissingPluginException)都会被 catch 捕获并返回兜底值,所以通道异常不会导致应用崩溃,但会让"跟随手机语言"静默失效——排查时优先看这里。

4.4.3 未识别的方法返回未实现

原生侧只处理 getPhoneLanguage,其余方法统一走 notImplemented(),与其余平台一致:

if (call.method == METHOD_GET_PHONE_LANGUAGE) {
  result.success(this.getPhoneLanguage());
} else {
  result.notImplemented();
}

保留这个分支的用意是让协议边界清晰:任何拼错的方法名都会立刻得到"未实现"的明确反馈,而不是静默返回空值。


五、补全 OHOS 原生实现与工程配置

5.1 在 AppLangPlugin.ets 中实现原生能力

以 getPhoneLanguage 为例:业务调用 AppLang().getPhoneLanguage() 时,Dart 通道发出同名命令。需要补全的是 AppLangPlugin.ets 中的对应分支:调用 i18n.System.getSystemLanguage() 读取系统语言,通过 result.success 回传语言标签,异常时通过 result.error 返回错误码。这样业务沿用 Locales.init 原有接口即可拿到 OHOS 设备的语言。

AppLangPlugin 实现 FlutterPlugin 和 MethodCallHandler。下面列出类中的主要成员和方法,完整文件还包含 getUniqueClassName() 等插件接口。

原生插件位于:

ohos/src/main/ets/components/plugin/AppLangPlugin.ets
5.1.1 引入 Flutter 和 HarmonyOS 能力
import {
  FlutterPlugin,
  FlutterPluginBinding,
  MethodCall,
  MethodCallHandler,
  MethodChannel,
  MethodResult,
} from '@ohos/flutter_ohos';
import i18n from '@ohos.i18n';
import { BusinessError } from '@ohos.base';
import hilog from '@ohos.hilog';

其中:

  • FlutterPlugin 负责接入 Flutter Engine 生命周期;
  • MethodChannel 接收 Dart 发来的命令;
  • i18n 提供系统语言查询能力,export default 的导出形式决定了要用默认导入;
  • BusinessError 用于读取 HarmonyOS 异常码和异常消息;
  • hilog 用于原生侧诊断日志。

这里没有 EventChannel 和 EventSink:语言切换是用户主动触发的 Dart 侧行为,不需要原生推送事件。

5.1.2 连接 Flutter Engine
private channel: MethodChannel | null = null;

onAttachedToEngine(binding: FlutterPluginBinding): void {
  try {
    this.channel = new MethodChannel(binding.getBinaryMessenger(), CHANNEL_NAME);
    this.channel.setMethodCallHandler(this);
    hilog.info(DOMAIN, TAG, 'attached to engine, channel [%{public}s] registered', CHANNEL_NAME);
  } catch (err) {
    const error = err as BusinessError;
    hilog.error(DOMAIN, TAG, 'onAttachedToEngine failed, code: %{public}d, message: %{public}s',
      error.code, error.message);
  }
}

onAttachedToEngine 只做三件事:创建通道、注册处理器、写一条日志。通道注册失败时吞掉异常会让问题隐藏到运行期,所以这里用 hilog.error 保留现场的异常码和消息。

5.1.3 读取系统语言标签
private getPhoneLanguage(): string {
  const language: string = i18n.System.getSystemLanguage();
  if (language != null && language.length > 0) {
    return language;
  }
  // 兜底:与 Dart 层默认值保持一致的语义
  return 'en';
}

选型说明:OpenHarmony 没有与 Android Locale.getDefault() 完全对等的 API,最贴近"手机语言"语义的是 i18n.System.getSystemLanguage(),它返回系统语言的 BCP-47 语言标签(如 zh-Hans)。备选的 i18n.System.getSystemLocale() 返回带国家/地区的完整区域(如 zh-Hans-CN),语义更偏向"区域";由于 Dart 层只取第一段,两者对上层等价,故选择语义更贴切的 getSystemLanguage()。

该接口无需申请任何权限。

5.1.4 没有常驻监听需要停止

本库不订阅系统事件,因此没有"取消订阅"这一步。getPhoneLanguage 是一次性调用,返回即结束,不持有回调、不占用系统资源。

真正需要释放的是通道本身和处理器引用,它们在 Engine 解绑时统一清理,见 5.1.6。如果后续要扩展"跟随系统语言实时变化"这类能力,才需要引入系统语言变化的监听,并在页面退出时停止——那属于新的能力设计,不在本次适配范围内。

5.1.5 处理 MethodChannel 命令
onMethodCall(call: MethodCall, result: MethodResult): void {
  try {
    hilog.debug(DOMAIN, TAG, 'onMethodCall: %{public}s', call.method);
    if (call.method == METHOD_GET_PHONE_LANGUAGE) {
      result.success(this.getPhoneLanguage());
    } else {
      result.notImplemented();
    }
  } catch (err) {
    const error = err as BusinessError;
    hilog.error(DOMAIN, TAG, 'onMethodCall [%{public}s] failed, code: %{public}d, message: %{public}s',
      call.method, error.code, error.message);
    result.error(ERR_CODE, 'Failed to handle ' + call.method + ': ' + error.message, error.message);
  }
}

result.error 将错误码和消息传回 Dart,触发 PlatformException;hilog 则保留原生侧的诊断信息。错误码使用常量 APP_LANG_ERROR,便于在日志中检索。Dart 侧的 catch 会把它转换为 en-OM 兜底。

5.1.6 Engine 解绑时释放资源
onDetachedFromEngine(binding: FlutterPluginBinding): void {
  try {
    if (this.channel != null) {
      // 置空 handler,避免引擎销毁后插件实例仍被通道持有而无法释放
      this.channel.setMethodCallHandler(null);
      this.channel = null;
    }
    hilog.info(DOMAIN, TAG, 'detached from engine');
  } catch (err) {
    const error = err as BusinessError;
    hilog.error(DOMAIN, TAG, 'onDetachedFromEngine failed, code: %{public}d, message: %{public}s',
      error.code, error.message);
  }
}

Flutter Engine 销毁时执行清理,注销通道处理器并释放引用,覆盖应用退出、多引擎切换等场景。

5.2 声明插件和宿主权限

本库的原生侧只做系统语言查询,不需要任何权限。

5.2.1 插件 HAR 的权限

插件 ohos/src/main/module.json5 中没有 requestPermissions 字段,模块信息保持如下:

{
  "module": {
    "name": "app_local",
    "type": "har",
    "deviceTypes": ["default", "tablet"]
  }
}

type 为 har,表示它作为静态库被宿主 entry 依赖。这里不添加权限声明,也不会因为缺少权限而出现功能受限。

5.2.2 应用 entry 的权限

宿主应用 example/ohos/entry/src/main/module.json5 中保留了模板自带的网络权限:

{
  "module": {
    "requestPermissions": [
      { "name": "ohos.permission.INTERNET" }
    ]
  }
}

这条声明是工程模板的默认项,与本库的语言查询和 JSON 加载都无关,删掉不影响示例运行。getPhoneLanguage 读取的 i18n.System.getSystemLanguage() 属于系统语言查询接口,无需申请权限;语言偏好的持久化由 shared_preferences 完成,写入应用沙箱,同样无需权限。

权限声明和运行时授权是两个步骤。若后续为插件新增能力,需要按目标 SDK 的权限定义处理授权;module.json5 中的声明不会自动完成运行时授权。

5.3 注册并导出插件

pubspec.yaml 通过以下配置声明 OHOS 插件类:

flutter:
  plugin:
    platforms:
      ohos:
        pluginClass: AppLangPlugin

原有 android、ios、macos 的注册项保持不变。

插件的 ohos/index.ets 需要导出实现:

import AppLangPlugin from './src/main/ets/components/plugin/AppLangPlugin';
export default AppLangPlugin;

执行 flutter pub get 和构建后,Flutter 工具会为应用生成插件注册代码。通常不应手工编辑 GeneratedPluginRegistrant.ets,因为下次构建可能覆盖它。

需要留意:本例生成的注册文件中只包含 AppLangPlugin,shared_preferences 的 OHOS 平台实现是否被正确解析注册,直接决定语言偏好能否持久化,核对方法见 8.5 和 9.4。

注册异常的排查步骤见第九节 MissingPluginException。

5.4 检查 example 的 OHOS 应用结构

本例的 example/ohos/build-profile.json5 应在 products 中设置版本。下面是需核对的配置片段,请合并到现有工程;其中 signingConfig: "default" 需要与本机配置的签名名称一致,签名材料保留在本地。

{
  "app": {
    "products": [
      {
        "name": "default",
        "signingConfig": "default",
        "compatibleSdkVersion": "6.1.1(24)",
        "targetSdkVersion": "6.1.1(24)",
        "runtimeOS": "HarmonyOS"
      }
    ]
  },
  "modules": [
    {
      "name": "entry",
      "srcPath": "./entry",
      "targets": [
        {
          "name": "default",
          "applyToProducts": ["default"]
        }
      ]
    }
  ]
}

配置后,在 DevEco Studio 中执行一次 Sync Project。如果 entry 模块正常识别,Project 视图中会出现 entry,并能打开 entry 模块的签名配置。

版本号要与目标设备对齐:本文设备为 API 24,因此两项都写 6.1.1(24)。沿用模板默认的 26.0.0 会导致安装时报版本不匹配,详见 9.7。


六、补全交付文件并提交适配分支

6.1 除代码外还要补全哪些文件

代码适配完成后,还需要整理安装说明、接口文档、版本记录和开源信息。接收仓库有专用模板时,按其格式填写:

文件应写清楚的内容
README.md原项目说明、OHOS 支持入口、配套 Demo 和文档链接;保留上游信息
README.OpenHarmony_CN.md简介、AtomGit 安装方式、版本对应关系、环境约束、权限、接口表、示例、已验证范围和遗留问题
README.OpenHarmony.md与中文说明对应的英文文档
CHANGELOG.OpenHarmony.mdOHOS 新增能力、适配版本、兼容限制与测试范围
LICENSE保留上游许可证,本库为 MIT
example/README.md依赖方式、运行目录、签名、操作步骤与效果图
pubspec.yaml、ohos/oh-package.json5核对包名、版本、插件注册、仓库地址、许可证和依赖
.gitignore忽略构建缓存及本机签名材料,不漏提交必要源码和配置

本库直接沿用上游 LICENSE,未额外增加 README.OpenSource 和 NOTICE。上游版权与许可证信息已在 README.OpenHarmony_CN.md 中说明:包名 app_local,版本 1.1.3,MIT 许可证,来源为 abom-me/app_local。

整理时还有两处要核对:ohos/oh-package.json5 的 license 字段目前仍是模板生成的 Apache-2.0,与上游 MIT 不一致,应改为与 LICENSE 相同;example/README.md 仍是模板默认内容,尚未写运行步骤。

6.2 提交前检查

提交前先完成第八节的插件、example 和真机测试,再从根目录检查改动:

git branch --show-current
git diff --check
git status --short
git diff --stat
git diff

本例的 diff 应包含:pubspec.yaml 的 ohos 平台声明、lib/plug/app_lang_method_channel.dart 的 Platform.isOhos 分支、新增的 ohos/ 与 example/ohos/,以及 OpenHarmony 说明与变更记录文件。检查 diff 时移除本机路径及签名信息,并使文档中的版本和分支与提交内容一致。

当前工作区中,example/ohos/build-profile.json5 已被 DevEco 写入本机证书路径与加密口令。这类内容属于个人环境信息,提交前必须把 signingConfigs 还原为空数组,不能进入仓库。

6.3 提交并推送到 AtomGit

文档和代码整理完成后,在根目录暂存并提交。文件名按项目实际情况调整:

git add ohos pubspec.yaml lib/plug/app_lang_method_channel.dart
git add example/lib/main.dart example/pubspec.yaml example/ohos
git add README.md README.OpenHarmony_CN.md README.OpenHarmony.md
git add CHANGELOG.OpenHarmony.md .gitignore
git diff --cached --check
git diff --cached --stat
git diff --cached
git commit -m "feat: add OHOS support for app_local 1.1.3"
git remote -v
git branch --show-current
git push -u origin main
git tag 1.1.3-ohos-1.0.0-beta.1
git push origin 1.1.3-ohos-1.0.0-beta.1

DevEco 可能向 example/ohos/build-profile.json5 写入本机签名配置,需要在提交前从暂存内容中移除——仓库中该文件的 signingConfigs 应保持为空数组。推送时,origin 应指向自己的 AtomGit 仓库,当前分支为 main。

推送后在 AtomGit 发起合并请求,说明上游来源和版本、OHOS 实现范围、依赖及权限、测试环境、操作结果、已知限制,并附 Demo 运行图。目标分支和评审流程以接收仓库要求为准。


七、使用根目录 example 演示接入

仓库自带 example/,可以直接用来调试插件和体验多语言切换。

7.1 本地适配时使用路径依赖

当前 example/pubspec.yaml 的依赖是:

dependencies:
  flutter:
    sdk: flutter
  app_local:
    path: ../

../ 相对于 example/pubspec.yaml 指向插件根目录,修改根目录插件后可直接联调。

7.2 通过 AtomGit 引入插件

业务应用通过 AtomGit 引入时,将 app_local 的 path 配置替换为下面的 Git 依赖。这里固定到本文使用的版本 TAG:

dependencies:
  flutter:
    sdk: flutter
  app_local:
    git:
      url: https://atomgit.com/oh-flutter/fluttertpc_app_local.git
      ref: 1.1.3-ohos-1.0.0-beta.1

使用自己的适配版本时,先推送分支与 TAG,再将 url 改为对应仓库,ref 改为自己的 TAG 或提交号。

从插件根目录执行:

cd example
flutter pub get
flutter pub deps

检查 example/pubspec.lock 中 app_local 的来源为 git,并核对 url、ref 和 resolved-ref。同时检查没有 dependency_overrides 或 pubspec_overrides.yaml 将其覆盖回本地依赖,确认应用使用的是 Git 依赖。

7.3 调用接口实现多语言页面

先在 pubspec.yaml 声明资源目录,并准备 assets/a/en.json 与 assets/a/ar.json 两份文案:

{
  "localization": "Localization",
  "welcome": "Welcome",
  "greeting": "Hello ##, welcome to ###!",
  "device_language": "Device language",
  "current_locale": "Current locale",
  "english": "English",
  "arabic": "Arabic",
  "auto": "Auto"
}

下面的页面展示初始化、文案显示和语言切换,可用于 example/lib/main.dart。仓库中的完整 Demo 还提供 RTL 判断、可用键列表和设置页。

import 'package:flutter/material.dart';
import 'package:app_local/app_local.dart';

void main() async {
  WidgetsFlutterBinding.ensureInitialized();
  await Locales.init(
    localeNames: ['en', 'ar'],
    localPath: "assets/a/",
    // phoneLocale: true // 首次启动时跟随手机语言
  );
  runApp(const MyApp());
}

class MyApp extends StatelessWidget {
  const MyApp({super.key});

  
  Widget build(BuildContext context) {
    return LocaleBuilder(
      builder: (locale) => MaterialApp(
        localizationsDelegates: Locales.delegates,
        supportedLocales: Locales.supportedLocales,
        locale: locale,
        home: const HomePage(),
      ),
    );
  }
}

class HomePage extends StatelessWidget {
  const HomePage({super.key});

  
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: const LocaleText('localization')),
      body: Padding(
        padding: const EdgeInsets.all(16),
        child: Column(
          crossAxisAlignment: CrossAxisAlignment.stretch,
          children: [
            LocaleText('welcome', style: const TextStyle(fontSize: 28)),
            const SizedBox(height: 8),
            // 占位符参数:对应 "greeting": "Hello ##, welcome to ###!"
            LocaleText(
              'greeting',
              params: [Locales.lang.toUpperCase(), 'app_local'],
            ),
            Text('Device language: ${Locales.deviceLocale}'),
            Text('Current locale: ${Locales.currentLocale(context)?.languageCode}'),
            const SizedBox(height: 16),
            Row(
              children: [
                Expanded(
                  child: FilledButton(
                    onPressed: () => context.changeLocale('en'),
                    child: const LocaleText('english'),
                  ),
                ),
                const SizedBox(width: 12),
                Expanded(
                  child: FilledButton.tonal(
                    onPressed: () => context.changeLocale('ar'),
                    child: const LocaleText('arabic'),
                  ),
                ),
              ],
            ),
          ],
        ),
      ),
    );
  }
}

phoneLocale: true 时首次启动会跟随手机语言(受支持时),否则使用上次保存的选择。切换语言由 LocaleBuilder 自动重建子树,无需重启应用。

7.4 页面退出时的处理

本库没有订阅、没有需要释放的原生资源,语言切换也没有异步回调,因此不需要在 dispose 中做清理,也不要在页面销毁后继续调用 changeLocale。

需要注意的是:语言选择已经由 shared_preferences 写入本地,应用重启后 Locales.init 会自动恢复上次的选择;开发期间反复调试时,如果发现"改了代码默认语言却没变",多半是设备上还残留着上次的偏好数据,卸载重装或在设置里选一次 auto 即可复位。

多个页面共用同一套语言状态时,LocaleBuilder 放在 MaterialApp 外层即可,各页面通过 Locales.string、LocaleText 或 context.localeString 取文案,不需要各自维护语言变量。


八、验证、构建与鸿蒙设备运行效果

8.1 分别验证插件与 example

从插件仓库根目录执行:

flutter pub get
flutter analyze
flutter test
cd example
flutter pub get
flutter analyze
flutter test

根目录 test/ 中的用例沿用了上游项目名(flutter_locales_test.dart),覆盖本地化初始化和键值读取,在 OHOS 工具链下以本机输出为准。Dart 测试覆盖接口和页面逻辑,语言检测通道及偏好持久化还需要在鸿蒙设备上验证。

另外可以用 diff 验证"Dart 层最小改动":上游与本仓库的差异应只有 pubspec.yaml 的 ohos 声明和 app_lang_method_channel.dart 的 Platform.isOhos 分支,其余 Dart 文件保持原样。

8.2 确认设备连接

hdc list targets
flutter devices

设备首次连接电脑时,需要在手机端确认调试授权。列表为空时,检查 USB 连接、调试模式和电脑授权。本文设备为 OpenHarmony-6.1.1.120(API 24)。

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

在 Windows 宿主上,若 flutter build hap 在 hvigor 的 ohpm install 阶段失败,可改用等价的直调方式(见 9.11):

cd example/ohos
hvigorw assembleHap -p product=default -p buildMode=debug --no-daemon

该命令需要 java 在 PATH 中,若命令行找不到 java,把 DevEco 自带的 <DevEco>/jbr/bin 加入 PATH。

典型产物位于:

example/ohos/entry/build/default/outputs/default/

目录中包含已签名和未签名 HAP(如 entry-default-signed.hap 与 entry-default-unsigned.hap)。真机安装应选择与当前设备匹配的已签名产物。产物同级 pack.info 的 apiVersion 应为 {compatible: 24, target: 24, releaseType: "Release"},与目标设备一致。

8.5 在设备上测试语言检测与切换

  1. 打开应用,确认首页正常显示英文文案,无白屏或异常;
  2. 查看"Device language"一行——本文设备系统语言为中文,应显示 zh,说明 getPhoneLanguage 通道在 OHOS 上生效;
  3. 查看"Current locale"与"Supported locales",确认当前语言与受支持列表正确;
  4. 进入 Setting 页,确认语言列表与当前选中项;
  5. 切换 Arabic,确认界面文案即时变化且布局按 RTL 排版;
  6. 切换回 English,再次确认文案恢复;
  7. 杀掉应用重新打开,确认恢复上次选择的语言;
  8. 多次来回切换,确认无卡顿、无崩溃。

其中第 7 步依赖 shared_preferences 在 OHOS 侧正确注册。核对方法:查看 example/ohos/entry/src/main/ets/plugins/GeneratedPluginRegistrant.ets 与 example/ohos 的依赖产物中是否存在 shared_preferences 的 OHOS 实现。若切换后重启总是回到默认语言,按 9.4 排查。

本文已在真机完成第 1–4 步(见 8.6,设备语言检测已验证),Arabic 切换(第 5 步)与偏好持久化(第 7 步)尚未留存截图,属待复核项。

8.6 鸿蒙设备运行效果

完成适配后,Flutter 应用在鸿蒙设备上可以正常完成 JSON 文案加载、设备语言检测和语言切换:任意受支持语言都能即时生效,RTL 语言按从右向左排版,全程无需重启应用。

下面展示真机运行时的页面效果:

示例首页(英文) 已加载的本地化键列表 语言设置页 示例首页(英文) 示例首页(英文) 示例首页(英文) 示例首页(英文) 示例首页(英文) 示例首页(英文) 示例首页(英文) 示例首页(英文) 示例首页(英文)

示例首页已加载键列表语言设置页
Device language 为 zh,Current locale 为 en共 25 个键,随 JSON 文件加载English 选中,支持 Auto 跟随手机语言

"Device language: zh"直接来自 app_lang 通道的返回值,说明 OHOS 侧的语言检测已经生效。本库不依赖设备硬件能力,不同机型的差异主要体现在系统语言设置,而不是库本身。


九、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 组件已经下载完整;
  2. 检查 Flutter 和 DevEco Studio 使用的 SDK 路径是否一致;
  3. 避免误用 /Applications/DevEco-Studio.app/Contents/sdk 之类的不完整目录,Windows 上同理不要指向 DevEco 安装目录下的中间目录;
  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 的 SDK 也能编译,因为 compatibleSdkVersion 声明的是最低兼容版本。

9.2 DevEco Studio 中看不到 entry 模块

插件的 ohos/ 目录是 HAR 模块,可安装应用的 entry 模块位于 example/ohos/entry。

请直接使用 DevEco Studio 打开:

fluttertpc_app_local/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 能安装但语言没变化或不记忆

按以下顺序检查:

  1. 是否命中了 4.4.2 的问题——lib/plug/app_lang_method_channel.dart 缺少 Platform.isOhos 分支,导致所有设备都返回 en-OM 兜底;
  2. JSON 资源目录是否在 pubspec.yaml 的 assets 中声明,localPath 是否以 / 结尾的目录路径;
  3. Locales.init 的 phoneLocale 参数是否按预期传入:true 才会在首次启动跟随手机语言;
  4. 设备语言是否在 supportedLocales 中:不支持时回落到列表第一个语言;
  5. 切换语言后重启不生效时,检查 shared_preferences 的 OHOS 平台实现是否被解析注册——查看 GeneratedPluginRegistrant.ets 和 example/ohos 的依赖产物;若没有对应实现,需要引入支持 OHOS 的 shared_preferences 适配版本;
  6. 设备上是否残留旧的偏好数据,卸载重装后重试。

9.5 MissingPluginException

这通常表示 Dart 通道找不到已注册的原生插件。新增原生插件后需要重新构建应用。从仓库根目录执行:

cd example
flutter clean
flutter pub get
flutter run -d <device-id>

如果仍然出现,检查自动生成的插件注册文件中是否包含 AppLangPlugin,同时核对 pubspec.yaml、ohos/index.ets 和 oh-package.json5。

需要说明的是:本库只有 getPhoneLanguage 走通道,即使出现该异常,应用也不会崩溃——Dart 侧 catch 会兜底返回 en-OM,表现为"跟随手机语言"静默失效。

9.6 多次切换语言会重复加载吗

不会产生额外加载。切换语言只更新 shared_preferences 中的偏好并通知 LocaleBuilder 重建,重建时 LocalesDelegate.load 才会读取对应语言的 JSON 文件;同一个语言重复切换时,Locales 实例按语言缓存文案,不会反复解析。

如果切换后界面文本短暂不变,确认 LocaleBuilder 包裹在 MaterialApp 外层,且文案是通过 LocaleText、Locales.string 或 context.localeString 读取的,而不是把翻译结果缓存在页面 State 里。

9.7 编译成功但安装失败

常见原因包括:

  • HAP 未签名或使用了错误的 Profile;
  • 设备未加入调试设备列表;
  • 包名与签名 Profile 不匹配;
  • 安装包的 compatibleSdkVersion 高于设备 API;
  • 手机上已经安装了使用不同证书签名的同包名应用;
  • 工程声明的 API 与设备 apiVersion 不一致,尤其是沿用模板默认的 26.0.0 而设备为 API 24。

根据安装错误码区分签名、版本和包名冲突,再处理对应配置。版本一项按 2.1 的规则改成设备的 版本(API) 形式即可。

9.8 flutter create 不认识 ohos,或包名不合法

先执行 flutter --version,确认使用的是 OHOS 版工具链,环境配置回到第二节的社区链接核对。包名报错时,确认当前目录包含目标插件的 pubspec.yaml,并显式传入 --project-name app_local;仓库目录名 fluttertpc_app_local 与 Dart 包名不同,不能直接当作包名使用。生成后检查 diff,清理模板注入的多余产物,再补充 ArkTS 业务实现。

9.9 AtomGit 依赖提示找不到分支、TAG 或无权限

先检查 URL 是否指向已同步的目标仓库,再确认 ref 中的 TAG 1.1.3-ohos-1.0.0-beta.1 已经推送。TAG 尚未推送时,可以先使用第 3.2 节的基线提交号加本地适配改动的方式联调。私有仓库还需在本机配置 Git 认证。

9.10 改了本地 ArkTS,Demo 为什么没变化

先检查 example/pubspec.yaml:Git 依赖读取远程提交,不会自动读取本地插件改动。本地联调切回 path: ../;测试远程版本则先提交推送,再更新依赖并核对 pubspec.lock 的 resolved-ref。原生代码变动后停止应用并重新构建运行,不能只做 Dart 热重载。

9.11 修改 JSON 文案后,界面文本没有更新

本地化文案在 LocalesDelegate.load 时读入内存,热重载不会重新执行它。确认两种生效方式:

  • 开发期修改 JSON 后执行 Hot Restart(不是 Hot Reload),让 Locales.init 和 delegate 重新加载;
  • 需要在运行时刷新已加载文案时,调用 Locales 实例的 reload()。

如果 Hot Restart 后仍是旧文案,检查修改的是否是当前语言对应的 JSON 文件(如 en.json 与 ar.json),以及 pubspec.yaml 的 assets 声明覆盖了该目录,改动资源后需要重新 flutter pub get 再构建。


相关链接

Logo

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

更多推荐