Flutter 插件鸿蒙化实战:app_local 多语言库 OHOS 平台适配全流程
开发工具: 华为云码道
原始仓库: 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 SDK | 3.47.7-ohos-1.0.0 | Flutter 编译与 OHOS 平台工具链 |
| Flutter 分支 | oh-3.47-dev | Flutter OHOS 对应开发分支 |
| Dart SDK | 3.13.3 | Dart 语言与包管理环境 |
| HarmonyOS 开发套件 | 26.0.0 release(26.0.0.851) | 本机 DevEco Studio 与安装的 SDK 版本(API 26) |
compileSdkVersion | 未在 product 中显式声明 | 编译时使用本机 DevEco 安装的 SDK |
targetSdkVersion | 6.1.1(24) | 应用面向的行为版本 |
compatibleSdkVersion | 6.1.1(24) | 当前工程声明的最低兼容版本 |
| 插件版本 | 1.1.3 | pubspec.yaml 中的包版本 |
| 原生语言 | ArkTS | HarmonyOS 插件实现 |
| 插件产物 | 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/。
图 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 / getPhoneLanguage | i18n.System.getSystemLanguage() | 返回 BCP-47 语言标签,如 zh-Hans、en |
| 语言码提取 | 无,纯 Dart 处理 | 不涉及原生代码 | 取标签第一段,如 zh-Hans-CN 取 zh |
| 不支持的平台/异常 | 无 | 不涉及原生代码 | 返回兜底值 en-OM,由业务回落到受支持语言 |
原生端需要保持通道名、方法名和返回值结构与其余平台一致。方法执行异常时,通过错误回调通知 Dart 层。
4.1 跨端架构与调用时序
Flutter 侧和 HarmonyOS 侧之间只有一条平台通道,语言切换和文案加载都在 Dart 层完成:
MethodChannel('app_lang'):只在初始化时调用一次getPhoneLanguage,获取手机语言;- 语言切换与 JSON 加载:
Locales在 Dart 层完成,shared_preferences负责记住用户选择。
MethodChannel 只服务于"手机语言是什么"这一个查询;用户切换语言、重建界面、加载文案都发生在 Dart 侧,与平台无关。
4.1.1 一次初始化与一次语言切换的时序
getPhoneLanguage 只在 Locales.init 和 PreferenceUtils.init 中被调用;切换语言不再经过通道。
4.2 语言标签模型:BCP-47 标签与语言码提取
原生 API 返回 BCP-47 语言标签,Dart 层只取第一段作为语言码:
deviceLocale =
(await AppLang().getPhoneLanguage() ?? "en-OM").split("-").first;
| 原生返回 | 提取结果 | 业务含义 |
|---|---|---|
zh-Hans | zh | 简体中文设备 |
zh-Hans-CN | zh | 带地区的中文标签,取段后等价 |
en-US | en | 英文设备 |
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 | 初始化本地化配置 | function | yes |
Locales.string | 按键取本地化文本,支持占位符参数 | function | yes |
Locales.change | 切换当前语言 | function | yes |
Locales.currentLocale / Locales.lang | 获取当前语言 | function / property | yes |
Locales.isDirectionRTL | 判断是否从右向左排版 | function | yes |
LocaleBuilder | 语言变化时重建子树 | widget | yes |
LocaleText | 本地化文本组件 | widget | yes |
context.changeLocale / String.localize | BuildContext 与 String 扩展 | extension | yes |
AppLang.getPhoneLanguage | 获取手机语言标签 | function | yes |
除 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.md | OHOS 新增能力、适配版本、兼容限制与测试范围 |
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 模块配置自动签名:
- 用 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
在 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 在设备上测试语言检测与切换
- 打开应用,确认首页正常显示英文文案,无白屏或异常;
- 查看"Device language"一行——本文设备系统语言为中文,应显示
zh,说明getPhoneLanguage通道在 OHOS 上生效; - 查看"Current locale"与"Supported locales",确认当前语言与受支持列表正确;
- 进入 Setting 页,确认语言列表与当前选中项;
- 切换 Arabic,确认界面文案即时变化且布局按 RTL 排版;
- 切换回 English,再次确认文案恢复;
- 杀掉应用重新打开,确认恢复上次选择的语言;
- 多次来回切换,确认无卡顿、无崩溃。
其中第 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 的版本是否匹配。
处理顺序:
- 在 DevEco Studio SDK Manager 中确认所需 API 组件已经下载完整;
- 检查 Flutter 和 DevEco Studio 使用的 SDK 路径是否一致;
- 避免误用
/Applications/DevEco-Studio.app/Contents/sdk之类的不完整目录,Windows 上同理不要指向 DevEco 安装目录下的中间目录; - 确认 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 的 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;defaultproduct 和 target 已正确关联;- 当前账号、证书和调试设备状态有效。
9.4 能安装但语言没变化或不记忆
按以下顺序检查:
- 是否命中了 4.4.2 的问题——
lib/plug/app_lang_method_channel.dart缺少Platform.isOhos分支,导致所有设备都返回en-OM兜底; - JSON 资源目录是否在
pubspec.yaml的assets中声明,localPath是否以/结尾的目录路径; Locales.init的phoneLocale参数是否按预期传入:true才会在首次启动跟随手机语言;- 设备语言是否在
supportedLocales中:不支持时回落到列表第一个语言; - 切换语言后重启不生效时,检查
shared_preferences的 OHOS 平台实现是否被解析注册——查看GeneratedPluginRegistrant.ets和example/ohos的依赖产物;若没有对应实现,需要引入支持 OHOS 的shared_preferences适配版本; - 设备上是否残留旧的偏好数据,卸载重装后重试。
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 再构建。
相关链接
更多推荐




所有评论(0)