Flutter/OpenHarmony 三方库适配实战:ni_links 深链兼容层

一、项目信息

项目内容
仓库名称ni_links
仓库地址https://atomgit.com/oh-flutter/ni_links
适配版本0.5.1-ohos.1
OHOS 原生入口UniLinksPlugin
方法通道uni_links/messages
事件通道uni_links/events
HAP 构建产物uni_links/example/build/ohos/hap/entry-default-unsigned.hap

uni_links 的业务目标很清晰:应用被外部链接启动时,业务代码需要拿到初始
URI;应用已经运行时,业务代码需要继续接收新的 URI。原库把这两个场景分别
抽象成 getInitialLink() 和 linkStream,因此 OHOS 适配的核心不是重写
Dart API,而是把 UIAbility 的 Want 转换成相同的平台通道契约。

二、先确认原库的通道契约

原库公开 API 很少,但平台边界非常明确:

Future<String?> getInitialLink() => UniLinksPlatform.instance.getInitialLink();

Stream<String?> get linkStream => UniLinksPlatform.instance.linkStream;

平台实现固定使用两条通道:

static const MethodChannel _mChannel = MethodChannel('uni_links/messages');
static const EventChannel _eChannel = EventChannel('uni_links/events');

这意味着适配工作需要同时满足两个条件:

  1. OHOS 原生插件必须响应 getInitialLink 方法。
  2. OHOS 原生插件必须在 uni_links/events 上提供可监听的事件流。

只实现 MethodChannel 会让冷启动链接可用,但 uriLinkStream 仍会因为没有
事件通道处理器而抛出 MissingPluginException。

三、让 Flutter OHOS 发现插件

根 pubspec.yaml 保留 Android、iOS 和 Web 的原有声明,只增加 OHOS
入口,并把版本调整为适配版本:

version: 0.5.1-ohos.1
homepage: https://atomgit.com/oh-flutter/ni_links

flutter:
  plugin:
    platforms:
      android:
        package: name.avioli.unilinks
        pluginClass: UniLinksPlugin
      ios:
        pluginClass: UniLinksPlugin
      web:
        default_package: uni_links_web
      ohos:
        pluginClass: UniLinksPlugin

OHOS HAR 模块使用 harTasks,不是应用工程的 appTasks:

import { harTasks } from '@ohos/hvigor-ohos-plugin';

export default {
  system: harTasks,
  plugins: [],
};

index.ets 负责向 Flutter 插件注册器导出原生类:

import UniLinksPlugin
  from './src/main/ets/components/plugin/UniLinksPlugin';

export default UniLinksPlugin;

pubspec.yaml、hvigorfile.ts 和 index.ets 是三个不同层的配置:
前者让 Flutter 找到插件,中间文件让 Hvigor 构建 HAR,最后的入口文件让
生成的 GeneratedPluginRegistrant.ets 能够实例化 ArkTS 插件。

四、ArkTS 插件的生命周期

插件同时实现 FlutterPlugin、MethodCallHandler、AbilityAware、
NewWantListener 和 StreamHandler:

const MESSAGES_CHANNEL = 'uni_links/messages';
const EVENTS_CHANNEL = 'uni_links/events';

export default class UniLinksPlugin
  implements
    FlutterPlugin,
    MethodCallHandler,
    AbilityAware,
    NewWantListener,
    StreamHandler {
  private methodChannel: MethodChannel | null = null;
  private eventChannel: EventChannel | null = null;
  private abilityBinding: AbilityPluginBinding | null = null;
  private initialLink: string | null = null;
  private eventSink: EventSink | null = null;

onAttachedToEngine 创建两条通道并注册处理器,onDetachedFromEngine
必须解绑处理器并清空状态:

onAttachedToEngine(binding: FlutterPluginBinding): void {
  this.methodChannel = new MethodChannel(
    binding.getBinaryMessenger(),
    MESSAGES_CHANNEL,
  );
  this.eventChannel = new EventChannel(
    binding.getBinaryMessenger(),
    EVENTS_CHANNEL,
  );
  this.methodChannel.setMethodCallHandler(this);
  this.eventChannel.setStreamHandler(this);
}

onDetachedFromEngine(binding: FlutterPluginBinding): void {
  this.methodChannel?.setMethodCallHandler(null);
  this.eventChannel?.setStreamHandler(null);
  this.methodChannel = null;
  this.eventChannel = null;
  this.eventSink = null;
}

Ability 绑定用于读取启动 Want,并注册热启动 Want 监听:

onAttachedToAbility(binding: AbilityPluginBinding): void {
  this.abilityBinding = binding;
  const ability = binding.getAbility();
  this.initialLink = this.extractLink(ability.launchWant);
  binding.addOnNewWantListener(this);
}

onDetachedFromAbility(): void {
  this.abilityBinding?.removeOnNewWantListener(this);
  this.abilityBinding = null;
}

这里的关键不是调用 ability.onNewWant,而是使用 Flutter OHOS embedding
提供的 AbilityPluginBinding。宿主 Ability 的生命周期事件由 embedding
转发到插件集合,插件只需注册一个 NewWantListener,避免直接依赖某个具体
宿主 Ability 的继承关系。

五、初始链接:从 launchWant 提取 URI

getInitialLink 直接返回 Ability 绑定阶段保存的 URI:

onMethodCall(call: MethodCall, result: MethodResult): void {
  switch (call.method) {
    case 'getInitialLink':
      result.success(this.initialLink);
      return;
    default:
      result.notImplemented();
  }
}

extractLink 只接受 Want.uri,没有 URI 时返回 null,不会把空字符串
当作有效链接:

private extractLink(want: Want | null): string | null {
  if (want === null || want.uri === undefined || want.uri === null) {
    return null;
  }
  return String(want.uri);
}

Dart 层无需改动即可继续使用:

final String? initialLink = await getInitialLink();
final Uri? initialUri = await getInitialUri();

初始链接只代表应用启动时的 Want。业务代码应在应用生命周期内只处理一次,
否则同一个冷启动 URI 可能被重复消费。

六、运行中链接:通过 EventSink 转发 onNewWant

当新的 Want 带有 URI 时,插件将字符串直接发送到事件通道:

onNewWant(want: Want, launchParams: AbilityConstant.LaunchParam): void {
  const link = this.extractLink(want);
  if (link !== null) {
    this.eventSink?.success(link);
  }
}

onListen(args: Any, events: EventSink): void {
  this.eventSink = events;
}

onCancel(args: Any): void {
  this.eventSink = null;
}

这段代码保留了 EventChannel 的生命周期语义:没有 Dart 监听器时不会持有
旧的 EventSink,取消订阅后也不会继续发送事件。没有 URI 的 Want 不会发送
null,因此 Dart 的 linkStream 不会因为普通生命周期 Want 产生伪链接。

七、示例工程与测试

示例页面同时展示初始 URI、最新 URI、路径和查询参数。页面初始化时分别
调用 getInitialUri() 和订阅 uriLinkStream:

void _handleIncomingLinks() {
  if (!kIsWeb) {
    _sub = uriLinkStream.listen((Uri? uri) {
      if (!mounted) return;
      setState(() {
        _latestUri = uri;
        _err = null;
      });
    });
  }
}

Future<void> _handleInitialUri() async {
  if (_initialUriIsHandled) return;
  _initialUriIsHandled = true;
  final Uri? uri = await getInitialUri();
  if (!mounted) return;
  setState(() => _initialUri = uri);
}

示例测试用平台测试二进制消息模拟两条通道:

messenger.setMockMethodCallHandler(messagesChannel, (call) async {
  if (call.method == 'getInitialLink') {
    return initialLink;
  }
  return null;
});

messenger.setMessageHandler(eventsChannel.name, (message) async {
  return const StandardMethodCodec().encodeSuccessEnvelope(null);
});

测试随后模拟一个热启动 URI:

await messenger.handlePlatformMessage(
  eventsChannel.name,
  const StandardMethodCodec().encodeSuccessEnvelope(nextLink),
  (_) {},
);

验证结果为:

00:00 +1: renders initial and subsequent links from platform channels

八、本地构建验证

当前环境完成的验证包括:

flutter pub get
flutter analyze
cd example
flutter pub get
flutter analyze
flutter test
flutter build hap --debug --no-codesign

结果:

检查项结果
根目录 flutter analyze通过,无问题
示例目录 flutter analyze通过,无问题
根目录 flutter test通过,4 个测试
示例目录 flutter test通过,1 个测试
HAP 构建通过
OHOS 虚拟机启动通过,应用进程稳定存活
冷启动 URI通过,getInitialLink() 返回启动 Want 的 URI
热启动 URI通过,onNewWant 将新 URI 发送到 linkStream

第一次构建曾因为插件 HAR 误用 appTasks 失败。将 hvigorfile.ts 改成
harTasks 后,Hvigor 才能按 HAR schema 构建。这个问题不是 Dart API
问题,而是模块类型和构建任务必须一致。

九、界面截图与能力边界

下面是示例应用在 OHOS 模拟器上的真实界面截图:

在这里插入图片描述

截图来自 emulator 7.0.0.107、系统 OHOS OpenHarmony-7.0.0.105。冷启动 URI
为 unilinks://example.com/path/subpath?uid=123,随后通过
onNewWant 发送 unilinks://example.com/products/42,页面的 Latest Uri
更新为第二个 URI。普通模拟器没有浏览器和第三方应用路由配置,因此外部应用
分发行为仍需真机验证。

当前能力边界:

  • 支持 UIAbility.launchWant.uri 冷启动链接。
  • 支持由 UIAbility.onNewWant 转发的运行中 URI。
  • 不解析 Want.parameters 中的自定义路由参数。
  • 不负责宿主工程的 URI 路由声明和系统权限配置。
  • 已验证模拟器内的 launchWant.uri 和 onNewWant 转发,不代表外部应用分发
    已在所有系统环境完成验证。

十、社区与贡献

欢迎加入 CPF-Flutter OpenHarmony 社区,
交流 Flutter 三方库适配、深链分发和虚拟机/真机验证经验。

Logo

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

更多推荐