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 入口主要方法
accAccountAuthServicesignIn、silentSignIn、signOut、cancelAuthorization、getChannel
acc/managerAccountAuthManagergetAuthResult、getAuthResultWithScopes、containScopes、addAuthScopes
hwid/authHuaweiIdAuthTooldeleteAuthInfo、requestUnionId、requestAccessToken
hwid/networkNetworkToolbuildNetworkCookie、buildNetworkUrl
hwid/smsReadSmsManagersmsVerification、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);
}

这里有两个适配约束:

  1. AccountKit 的字段名是 openID、unionID,而 Flutter 模型字段名是 openId、unionId。
  2. 登录接口返回的是身份凭证,不等于用户资料接口。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_unsupportedOHOS 没有对应 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 工程配置。构建过程中遇到过三个实际问题:

  1. 示例根目录缺少 oh-package.json5,Hvigor 直接报告 JSON/JSON5 解析失败。
  2. 复制模板时多出一层 entry/src/main/resources/resources,资源工具把它识别成非法限定符目录。
  3. 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:

组件版本
Flutter3.44.9+ohos-0.0.1-canary1
Dart3.12.2
DevEco StudioHarmonyOS 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
启动 AbilityEntryAbility
HAPentry-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

Logo

开源鸿蒙跨平台开发社区汇聚开发者与厂商,共建“一次开发,多端部署”的开源生态,致力于降低跨端开发门槛,推动万物智联创新。

更多推荐