Flutter三方库适配OpenHarmony【flutter_web_auth】— openLink API 与浏览器启动策略

前言

欢迎加入开源鸿蒙跨平台社区:https://openharmonycrossplatform.csdn.net

打开浏览器这件事,在 Android 上一行 startActivity(intent) 就搞定了。在 OpenHarmony 上稍微复杂一点------首选方案是 context.openLink(url),如果失败了还有 context.startAbility(want) 作为降级。flutter_web_auth 实现了一个 try-catch-fallback 模式来确保浏览器一定能打开。

一、context.openLink(url) API 详解

1.1 API 签名

typescript 复制代码
openLink(link: string, options?: OpenLinkOptions): Promise<void>

1.2 基本用法

typescript 复制代码
context.openLink(url)
  .then(() => {
    console.info(TAG, 'openLink succeeded, browser should be open');
  })
  .catch((err: BusinessError) => {
    console.error(TAG, `openLink failed: ${err.code} ${err.message}`);
  });

1.4 与 Android startActivity 的对比

维度 Android startActivity OpenHarmony openLink
同步/异步 同步 异步
错误反馈 抛异常 Promise reject
参数 Intent 对象 URL 字符串
灵活性 高(可配置 Intent) 中(只接受 URL)

📌 openLink 是异步的 ,这意味着调用后不能立即假设浏览器已经打开。需要通过 .then() 确认成功,通过 .catch() 处理失败。

2.1 flutter_web_auth 中的实现

typescript 复制代码
private authenticate(url: string, callbackUrlScheme: string, result: MethodResult): void {
  FlutterWebAuthPlugin.callbacks.set(callbackUrlScheme, result);

  let context: common.UIAbilityContext | null = null;
  if (this.ability) {
    context = this.ability.context;
  }

  if (!context) {
    FlutterWebAuthPlugin.callbacks.delete(callbackUrlScheme);
    result.error("NO_CONTEXT", "Unable to get UIAbilityContext.", null);
    return;
  }

  try {
    context.openLink(url)
      .then(() => {
        console.info(TAG, 'openLink succeeded');
      })
      .catch((err: BusinessError) => {
        console.error(TAG, `openLink failed: ${err.code} ${err.message}`);
        this.startAbilityFallback(context!, url, callbackUrlScheme, result);
      });
  } catch (e) {
    this.startAbilityFallback(context, url, callbackUrlScheme, result);
  }
}

2.2 双重错误处理

复制代码
try {
  context.openLink(url)        ← 可能同步抛异常
    .then(...)                  ← 成功
    .catch(err => ...)          ← 异步失败
} catch (e) {                   ← 同步异常
  fallback(...)
}
错误类型 捕获方式 示例
同步异常 try-catch openLink 方法不存在(低版本 API)
异步失败 Promise.catch URL 格式错误、无可用浏览器

2.3 为什么需要两层错误处理

typescript 复制代码
// 场景1:openLink 方法本身抛异常(同步)
// 可能在低版本 API 上 openLink 不存在
try {
  context.openLink(url);  // TypeError: openLink is not a function
} catch (e) {
  // 被 try-catch 捕获
}

// 场景2:openLink 调用成功但执行失败(异步)
context.openLink("invalid-url")
  .catch((err) => {
    // 被 Promise.catch 捕获
  });

💡 这种 try-catch + Promise.catch 的双重保护模式,在 OpenHarmony 开发中很常见。因为有些 API 在不同版本上的行为不一致。

三、startAbility 降级方案

3.1 实现

typescript 复制代码
private startAbilityFallback(
  context: common.UIAbilityContext,
  url: string,
  callbackUrlScheme: string,
  result: MethodResult
): void {
  let want: Want = {
    action: 'ohos.want.action.viewData',
    uri: url,
  };

  context.startAbility(want)
    .then(() => {
      console.info(TAG, 'startAbility fallback succeeded');
    })
    .catch((err: BusinessError) => {
      console.error(TAG, `startAbility fallback also failed: ${err.code} ${err.message}`);
      FlutterWebAuthPlugin.callbacks.delete(callbackUrlScheme);
      result.error("LAUNCH_FAILED", `Failed to open URL: ${err.code} - ${err.message}`, null);
    });
}

3.2 Want 参数

typescript 复制代码
let want: Want = {
  action: 'ohos.want.action.viewData',  // 查看数据
  uri: url,                              // 要打开的 URL
};
字段 值 作用
action ohos.want.action.viewData 告诉系统"我要查看这个 URL"
uri https://auth.example.com/... 要打开的认证页面
维度 openLink startAbility
参数 URL 字符串 Want 对象
灵活性 低 高
推荐度 ✅ 推荐 ⚠️ 降级方案
API 版本 较新 较早

3.4 降级策略流程图

复制代码
authenticate(url, scheme)
    │
    ├── context == null?
    │       └── YES → error("NO_CONTEXT") → 结束
    │
    └── context != null
            │
            ├── try openLink(url)
            │       │
            │       ├── .then() → 成功,等待回调
            │       │
            │       └── .catch() → 失败
            │               │
            │               └── startAbilityFallback()
            │                       │
            │                       ├── .then() → 成功,等待回调
            │                       │
            │                       └── .catch() → 彻底失败
            │                               │
            │                               └── error("LAUNCH_FAILED")
            │
            └── catch (同步异常)
                    │
                    └── startAbilityFallback()(同上)

4.1 一个容易忽略的细节

typescript 复制代码
context.openLink(url)
  .then(() => {
    console.info(TAG, 'openLink succeeded, browser should be open');
    // 注意:这里没有调用 result.success()
    // 因为认证还没完成,需要等待深度链接回调
  });

openLink 成功只意味着浏览器打开了,不意味着认证完成了。认证结果要等到用户在浏览器中完成操作,浏览器重定向到回调 URL,系统通过深度链接唤起 App 后才能拿到。

4.2 等待期间的状态

复制代码
openLink 成功
    ↓
浏览器打开认证页面
    ↓
App 进入后台(或保持前台但失去焦点)
    ↓
callbacks Map 中有一个 pending 的 MethodResult
    ↓
等待 onNewWant 被调用...

4.3 等待期间可能发生的事

事件 处理
用户完成认证 onNewWant → success
用户取消(切回 App) resumed → cleanUpDanglingCalls → error("CANCELED")
App 被系统杀掉 callbacks 丢失,认证失败
用户很久不操作 一直等待,直到用户回来

五、与 Android startActivity(Intent.ACTION_VIEW) 的对比

5.1 Android 实现

kotlin 复制代码
// Android
val intent = Intent(Intent.ACTION_VIEW, Uri.parse(url))
context.startActivity(intent)

5.2 OpenHarmony 实现

typescript 复制代码
// OpenHarmony - 方案1
context.openLink(url);

// OpenHarmony - 方案2(降级)
context.startAbility({
  action: 'ohos.want.action.viewData',
  uri: url,
});

5.3 对比表

维度 Android OpenHarmony (openLink) OpenHarmony (startAbility)
参数类型 Intent String Want
同步/异步 同步 异步 异步
错误处理 try-catch Promise.catch Promise.catch
浏览器选择 系统选择 系统选择 系统选择
推荐度 ✅ ✅ ⚠️

6.1 常见失败原因

原因 错误码 解决方案
URL 格式无效 参数错误 校验 URL 格式
没有可用的浏览器 匹配失败 降级到 startAbility
网络权限未声明 权限错误 添加 INTERNET 权限
API 版本不支持 方法不存在 降级到 startAbility

6.2 网络权限

json5 复制代码
// 宿主应用的 module.json5
{
  "module": {
    "requestPermissions": [
      { "name": "ohos.permission.INTERNET" }
    ]
  }
}

📌 INTERNET 权限必须在宿主应用中声明,不是在插件的 module.json5 中。插件的 module.json5 只声明模块元数据,不声明权限。

6.3 URL 格式要求

typescript 复制代码
// ✅ 合法的 URL
context.openLink("https://accounts.google.com/o/oauth2/v2/auth?...");

// ❌ 不合法的 URL
context.openLink("not a url");  // 可能失败
context.openLink("");            // 空字符串

七、浏览器启动的用户体验

7.1 启动流程的用户感知

复制代码
1. 用户点击"登录"按钮
2. 短暂的加载动画(openLink 异步)
3. 系统浏览器打开,显示认证页面
4. 用户在浏览器中操作
5. 认证完成,自动跳回 App

7.2 优化建议

优化项 做法 效果
加载提示 调用 authenticate 后显示 loading 用户知道在等待
超时处理 设置合理的超时时间 避免无限等待
错误提示 捕获 PlatformException 告诉用户发生了什么
dart 复制代码
// Dart 层的用户体验优化
setState(() => _isLoading = true);
try {
  final result = await FlutterWebAuth.authenticate(
    url: authUrl,
    callbackUrlScheme: scheme,
  );
  // 处理结果
} on PlatformException catch (e) {
  if (e.code == 'CANCELED') {
    // 用户取消,不需要提示
  } else {
    // 显示错误提示
    showSnackBar('登录失败: ${e.message}');
  }
} finally {
  setState(() => _isLoading = false);
}

总结

本文详细分析了 flutter_web_auth 的浏览器启动策略:

  1. openLink:推荐方案,异步打开系统浏览器
  2. startAbility:降级方案,通过 Want 打开浏览器
  3. 双重错误处理:try-catch + Promise.catch
  4. 等待机制:openLink 成功后等待深度链接回调
  5. INTERNET 权限:必须在宿主应用中声明

下一篇我们讲静态回调 Map------认证结果是怎么从 onNewWant 传回 Dart 的。

如果这篇文章对你有帮助,欢迎点赞👍、收藏⭐、关注🔔,你的支持是我持续创作的动力!


相关资源:

相关推荐
好的佩奇0110 小时前
Dart 之类修饰符
android·flutter·dart
Jewel10511 小时前
文字为什么总是不居中!记录一个bug
android·flutter·ios
2601_949847751 天前
Form-data 跨端三方库仓颉适配全流程
前端·javascript·flutter
大龄秃头程序员1 天前
Flutter 中我为什么把 GetX 定位为高效率方案,而不是默认架构方案
flutter
传奇开心果编程1 天前
组合优先开发范式:从汉字造字原理到声明式UI组件组合思想
android·flutter·ios·swiftui·移动开发·harmonyos·android jetpack
2501_944521592 天前
Ramda 跨端三方库仓颉适配全流程
开发语言·javascript·flutter·php
大龄秃头程序员2 天前
Flutter 架构里总被混淆的 IoC、DI、Service Locator,到底是什么关系?
flutter
事圆则缓2 天前
Flutter 与原生交互的最后一层:RTC 视频帧怎么走
flutter·音视频·实时音视频
2601_949847752 天前
jose 跨端三方库仓颉适配全流程
前端·javascript·flutter
2601_949970692 天前
msgpackr 跨端三方库仓颉适配全流程
前端·javascript·flutter