Flutter 3.47 鸿蒙三方库适配实战:flutter_hms_account适配
Flutter 3.47 鸿蒙三方库适配实战:flutter_hms_account适配
一、项目信息
| 项目 | 内容 |
|---|---|
| 仓库名称 | flutter_hms_account |
| 仓库地址 | https://atomgit.com/oh-flutter/flutter_hms_account |
| 原始包名 | huawei_account |
| 原始版本 | 6.12.0+301 |
| OHOS 版本 | 6.12.0+301-ohos.1 |
| 原始项目 | HMS-Core/hms-flutter-plugin |
| OHOS 原生入口 | HmsAccountPlugin |
| 构建产物 | entry-default-unsigned.hap |
huawei_account 是 HMS Flutter 插件中的账号子包。原始包只声明了 Android 平台,Dart 侧的 AccountAuthService、AccountAuthManager、HuaweiIdAuthTool、NetworkTool 和 ReadSmsManager 全部通过 MethodChannel 调用原生实现。
这次适配的目标不是重写账号 SDK,而是让这些已有 Dart API 在 OHOS 上继续工作:华为账号登录、静默登录、scope 授权查询、追加授权和取消授权使用 AccountKit;Android 专用的独立登录、accessToken、短信自动读取则明确返回 unsupported,避免伪造平台能力。
二、先确认原库的通道契约
原库的常量集中在 lib/src/utils/constants.dart,一共五条通道:
com.huawei.hms.flutter.account/acc
com.huawei.hms.flutter.account/acc/manager
com.huawei.hms.flutter.account/hwid/auth
com.huawei.hms.flutter.account/hwid/network
com.huawei.hms.flutter.account/hwid/sms
对应的 Dart 调用关系如下:
| 通道 | Dart 入口 | 主要方法 |
|---|---|---|
acc | AccountAuthService | signIn、silentSignIn、signOut、cancelAuthorization、getChannel |
acc/manager | AccountAuthManager | getAuthResult、getAuthResultWithScopes、containScopes、addAuthScopes |
hwid/auth | HuaweiIdAuthTool | deleteAuthInfo、requestUnionId、requestAccessToken |
hwid/network | NetworkTool | buildNetworkCookie、buildNetworkUrl |
hwid/sms | ReadSmsManager | smsVerification、smsWithPhoneNumber、obtainHashcode |
这里有一个容易漏掉的细节:Dart 的 AuthAccount 不是直接把 AccountKit credential 透传回来,而是要求固定字段名,例如 openId、unionId、authorizedScopes、avatarUri。如果 OHOS 侧返回 openID、unionID 或只返回 credential 对象,Dart 侧就会拿到 null。
三、让 Flutter OH 发现账号插件
根 pubspec.yaml 保留原有 Android 声明,并增加 OHOS 入口:
version: 6.12.0+301-ohos.1
flutter:
plugin:
platforms:
android:
package: com.huawei.hms.flutter.account
pluginClass: HmsAccountPlugin
ohos:
package: com.huawei.hms.flutter.account
pluginClass: HmsAccountPlugin
OHOS HAR 模块使用 harTasks:
import { harTasks } from '@ohos/hvigor-ohos-plugin';
export default {
system: harTasks,
plugins: [],
};
ohos/index.ets 只负责向 Flutter 插件注册器导出 ArkTS 类:
import HmsAccountPlugin
from './src/main/ets/components/plugin/HmsAccountPlugin';
export default HmsAccountPlugin;
Flutter 工具根据 pubspec.yaml 的 ohos 配置生成 GeneratedPluginRegistrant.ets,示例工程只需在 EntryAbility 中调用:
configureFlutterEngine(flutterEngine: FlutterEngine) {
super.configureFlutterEngine(flutterEngine);
GeneratedPluginRegistrant.registerWith(flutterEngine);
}
四、五条通道集中到一个生命周期对象
账号插件的生命周期由 HmsAccountPlugin 管理:
const AUTH_SERVICE_CHANNEL =
'com.huawei.hms.flutter.account/acc';
const AUTH_MANAGER_CHANNEL =
'com.huawei.hms.flutter.account/acc/manager';
const AUTH_TOOL_CHANNEL =
'com.huawei.hms.flutter.account/hwid/auth';
const NETWORK_TOOL_CHANNEL =
'com.huawei.hms.flutter.account/hwid/network';
const SMS_MANAGER_CHANNEL =
'com.huawei.hms.flutter.account/hwid/sms';
onAttachedToEngine 创建五条 MethodChannel,统一转发到 AccountMethodCallHandler。这样五条通道共享同一个 UIAbility context 和最近一次授权缓存:
onAttachedToEngine(binding: FlutterPluginBinding): void {
this.handler = new AccountMethodCallHandler();
for (const channelName of channelNames) {
const channel = new MethodChannel(
binding.getBinaryMessenger(),
channelName,
);
channel.setMethodCallHandler({
onMethodCall: (call, result): void => {
this.handler?.handle(channelName, call, result);
},
});
this.channels.push(channel);
}
}
AbilityAware 的作用不是处理业务,而是取得 common.UIAbilityContext:
onAttachedToAbility(binding: AbilityPluginBinding): void {
this.context = binding.getAbility().context;
this.handler?.setContext(this.context);
}
onDetachedFromAbility(): void {
this.handler?.setContext(null);
this.context = null;
}
onDetachedFromEngine 会解绑所有通道并清空 handler,避免热重载或页面销毁后继续持有 Ability context。
五、登录:LoginWithHuaweiIDRequest 与 AuthAccount 映射
signIn 和 silentSignIn 都走 AccountKit 的登录请求,唯一差别是 forceLogin:
private async executeLogin(
forceLogin: boolean,
result: MethodResult,
): Promise<void> {
const provider = new authentication.HuaweiIDProvider();
const request = provider.createLoginWithHuaweiIDRequest();
request.forceLogin = forceLogin;
const response:
authentication.LoginWithHuaweiIDResponse =
await new authentication.AuthenticationController(this.context!)
.executeRequest(request) as
authentication.LoginWithHuaweiIDResponse;
const credential = response.data;
this.authResult = {
authorizationCode: credential.authorizationCode,
authorizedScopes: ['openid'],
idToken: credential.idToken,
openId: credential.openID,
unionId: credential.unionID,
avatarUri: null,
displayName: null,
email: null,
} as ESObject;
result.success(this.authResult);
}
这里有两个适配约束:
- AccountKit 的字段名是
openID、unionID,而 Flutter 模型字段名是openId、unionId。 - 登录接口返回的是身份凭证,不等于用户资料接口。
avatarUri、displayName、email需要通过带 scope 的授权请求获取。
因此登录缓存中的 authorizedScopes 固定为 openid,不会把没有请求过的 profile/email 标记为已授权。
六、授权查询和追加 scope
带 scope 的接口走 AuthorizationWithHuaweiIDRequest:
private async executeAuthorization(
call: MethodCall,
result: MethodResult,
): Promise<void> {
const scopes = this.stringListArgument(call, 'scopes');
const request = new authentication.HuaweiIDProvider()
.createAuthorizationWithHuaweiIDRequest();
request.scopes = scopes.length > 0 ? scopes : ['openid'];
const response:
authentication.AuthorizationWithHuaweiIDResponse =
await new authentication.AuthenticationController(this.context!)
.executeRequest(request) as
authentication.AuthorizationWithHuaweiIDResponse;
const credential = response.data;
this.authResult = {
authorizationCode: credential.authorizationCode,
authorizedScopes: credential.authorizedScopes ?? scopes,
avatarUri: credential.avatarUri,
displayName: credential.nickName,
email: credential.email,
idToken: credential.idToken,
openId: credential.openID,
unionId: credential.unionID,
} as ESObject;
result.success(this.authResult);
}
containScopes 不需要再次打开授权页面,只检查传入 account 或缓存 account 的 authorizedScopes:
final bool granted = await AccountAuthManager.containScopes(
account,
<Scope>[Scope.profile, Scope.email],
);
业务代码通常应该先检查 scope,再决定是否调用 addAuthScopes。这样可以避免每次进入页面都重复打开授权窗口。
七、取消授权和错误语义
signOut 和 cancelAuthorization 在 OHOS 统一走 CancelAuthorizationRequest,成功后清空内存缓存:
private async executeCancel(result: MethodResult): Promise<void> {
const provider = new authentication.HuaweiIDProvider();
const request = provider.createCancelAuthorizationRequest();
await new authentication.AuthenticationController(this.context!)
.executeRequest(request);
this.authResult = null;
result.success(true);
}
错误处理需要保留 AccountKit 的业务错误码:
| 错误码 | 场景 |
|---|---|
1001502001 | 用户没有登录华为账号 |
1001502002 | 应用没有获得授权 |
1001502012 | 用户取消授权 |
1001502014 | 应用没有申请所需 scope 或权限 |
hms_account_no_context | 插件还没有绑定 UIAbility |
hms_account_unsupported | OHOS 没有对应 Android API |
这样 Dart 侧继续使用原来的 PlatformException 处理方式,不需要为 OHOS 增加一套全新的异常类型。
八、Android 专用能力为什么不硬做
下面三个 API 在 Android 上依赖 HMS Account SDK 的 Android 账号体系:
independentSignIn
requestAccessToken
deleteAuthInfo
OHOS AccountKit 没有同语义的公开接口,因此适配层返回:
hms_account_unsupported
ReadSmsManager 也做了同样的边界处理。Android 的短信验证码自动读取依赖短信权限和系统广播,OHOS 的权限模型和广播规则不同,不能把 Android 的监听结果直接搬到 ArkTS。
NetworkTool 则不同,它只是构造 Cookie 字符串和 URL,完全可以在 ArkTS 中按相同参数语义实现:
const parts: string[] = [`${name}=${value ?? ''}`];
if (path !== null && path !== '') {
parts.push(`Path=${path}`);
}
if (domain !== null && domain !== '') {
parts.push(`Domain=${domain}`);
}
if (isHttpOnly === true) {
parts.push('HttpOnly');
}
这种“能兼容就兼容,不能兼容就明确报错”的边界,比返回假数据更容易让业务代码稳定。
九、示例工程与 ArkTS 编译问题
示例工程保留上游 example/lib/main.dart 的账号按钮和日志列表,并补充 OHOS 工程配置。构建过程中遇到过三个实际问题:
- 示例根目录缺少
oh-package.json5,Hvigor 直接报告 JSON/JSON5 解析失败。 - 复制模板时多出一层
entry/src/main/resources/resources,资源工具把它识别成非法限定符目录。 - AccountKit response 的静态类型是
AuthenticationResponse,直接访问data会触发 ArkTS 编译错误,需要显式转换为登录或授权 response 类型。
修复后的核心类型写法是:
const response:
authentication.LoginWithHuaweiIDResponse =
await controller.executeRequest(request) as
authentication.LoginWithHuaweiIDResponse;
ArkTS 严格模式还禁止用类型谓词写 filter((item): item is string => ...),所以 scope 列表使用 Array.isArray 加显式 as string[] 转换。
十、构建和验证结果
本次本地验证使用 Flutter OH SDK:
| 组件 | 版本 |
|---|---|
| Flutter | 3.44.9+ohos-0.0.1-canary1 |
| Dart | 3.12.2 |
| DevEco Studio | HarmonyOS SDK API 26 |
| 构建模式 | --debug --no-codesign |
执行:
flutter pub get
flutter analyze
cd example
flutter pub get
flutter analyze
flutter build hap --debug --no-codesign
结果:
| 检查项 | 结果 |
|---|---|
根目录 flutter analyze | 通过,0 issues |
示例目录 flutter analyze | 通过,0 issues |
| OHOS HAP 构建 | 通过,生成 entry-default-unsigned.hap |
本次已在 OpenHarmony 模拟器上完成安装和启动验证,设备信息如下:
| 项目 | 实测值 |
|---|---|
| hdc 设备 | 127.0.0.1:5555 |
| 系统版本 | OpenHarmony-7.0.0.105 (API 26) |
| 应用包名 | com.huawei.hms.flutter.account.example |
| 启动 Ability | EntryAbility |
| HAP | entry-default-unsigned.hap |
安装和启动命令:
hdc install -r \
example/build/ohos/hap/entry-default-unsigned.hap
hdc shell aa start -a EntryAbility \
-b com.huawei.hms.flutter.account.example
首屏截图显示 Flutter 页面、AccountKit 操作按钮和日志区域都已正常渲染,说明插件注册、MethodChannel 创建和示例 UI 加载链路正常:
图 1:模拟器上的 huawei_account 示例首屏,插件已成功注册并显示账号操作入口。
点击 SILENT SIGN IN 后,示例收到 AccountKit 的业务错误:
PlatformException(
1001502003,
Invalid input parameter value. Invalid clientId or profile.,
null,
null,
)
截图:
图 2:静默登录触发后的错误日志。错误由 AccountKit 返回,说明 Dart 到 ArkTS 的通道调用已经完成。
图 3:显式登录触发后的错误日志,后续只需补配有效的 HUAWEI ID Client ID、profile 和签名配置。
因此当前验证结论是:
- 适配链路正常:HAR 注册、五条 MethodChannel、Ability context 和 AccountKit 请求均可执行。
- 业务配置未完成:示例尚未配置有效的
clientId/profile,所以 AccountKit 按预期返回1001502003,不能伪造登录成功结果。 - 后续验证:在华为开发者服务中配置账号应用的 Client ID、profile、包名和签名证书后,再补一张真实登录成功截图。
十一、复用到其他 HMS 子插件
这次适配最重要的结构经验是“按 Dart 契约分组原生通道”:
一个 FlutterPlugin
-> 多个 MethodChannel
-> 一个共享 MethodCallHandler
-> 共享 UIAbilityContext 和结果缓存
HMS Flutter monorepo 的子插件往往有多个原生通道,但它们的生命周期和用户状态是共享的。把 context、缓存和错误映射集中在一个 handler 中,可以避免每个通道各自复制一遍生命周期代码,也能让 unsupported API 的边界清晰可审查。
十二、参考资料和仓库地址
欢迎加入 OpenHarmony Flutter 社区,一起补充更多 HMS 子插件的 OHOS 适配:https://atomgit.com/CPF-Flutter
更多推荐



所有评论(0)