Flutter/OpenHarmony 三方库适配实战:ni_links 深链兼容层
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');
这意味着适配工作需要同时满足两个条件:
- OHOS 原生插件必须响应
getInitialLink方法。 - 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 三方库适配、深链分发和虚拟机/真机验证经验。
更多推荐



所有评论(0)