
基于 Flutter-OH 3.44.9-devDart 3.12.2在 Windows 10 22H2 上全程实测通过开发工具 DevEco Studio 26HarmonyOS SDK compatibleSdkVersion 5.1.0(18) / compileSdkVersion 26.0.0.105真机环节在一台**鸿蒙 PCOpenHarmony 6.1.1API 24arm642in1 形态**上验证。文中所有源码分析、改动内容、构建输出、真机效果均为实际环境抓取可放心对照复现。前言OpenHarmony × Flutter 社区资源社区主页CPF-Flutter —— OpenHarmony Flutter 生态维护组织Flutter-OH 主仓库CPF-Flutter/flutter_flutter —— SDK 与 Engine 的 OpenHarmony 适配版本三方库适配与开发文档OpenHarmony-Flutter —— 生态库适配清单与开发规范官方示例与开发指南openharmony-tpc/flutter_samples —— 环境搭建指导、FAQ 与示例工程鸿蒙化适配仓库oh-flutter/onesignal_flutter —— 本文 onesignal_flutter 的 OpenHarmony 适配版本环境搭好之后还没搭的看这篇 Flutter 鸿蒙环境搭建指南下一步就是给项目引入三方库。本文以 onesignal_flutter推送通知插件为例完整演示从fork 原仓库 → 编写 ArkTS 原生插件 → flutter create 生成宿主 → 构建 → 真机验证的全流程。这是一个9 个 MethodChannel、50 方法的复杂插件比 in_app_update 的单 Channel 场景更具代表性。OneSignal 是海外主流的免费推送通知服务类似国内的极光推送、个推提供推送通知、应用内消息、用户标签管理、会话追踪等能力。鸿蒙上没有 OneSignal SDK必须用 ArkTS 原生桩实现来模拟全部接口。一、插件分析onesignal_flutter 做了什么1.1 基本信息onesignal_flutter 是 OneSignal 官方 Flutter SDKAndroid 和 iOS 侧分别对接各自的 OneSignal Native SDK。项目内容库名onesignal_flutter适配基于版本5.6.10上游 pub.dev 最新 5.7.1适配仓库基于 5.6.10 fork功能推送通知、应用内消息、用户标签、会话追踪主要 APIinitialize()、login()、addTags()、consentGiven()、Notifications.addClickListener() 等原生依赖Android: OneSignal Android SDKiOS: OneSignal iOS SDK1.2 为什么这个库必须写原生代码onesignal_flutter 的 Dart 层完全通过 9 个 MethodChannel 调用原生能力// lib/onesignal_flutter.dart原插件源码 static MethodChannel _channel const MethodChannel(OneSignal); static Futurevoid initialize(String appId) async { await _channel.invokeMethod(OneSignal#initialize, {appId: appId}); await Future.wait([ InAppMessages.lifecycleInit(), User.lifecycleInit(), User.pushSubscription.lifecycleInit(), Notifications.lifecycleInit(), ]); }// lib/src/notifications.dart MethodChannel _channel const MethodChannel(OneSignal#notifications); Futurevoid lifecycleInit() async { _permission await _channel.invokeMethod(OneSignal#permission); return await _channel.invokeMethod(OneSignal#lifecycleInit); }// lib/src/pushsubscription.dart MethodChannel _channel const MethodChannel(OneSignal#pushsubscription); Futurevoid lifecycleInit() async { _token await _channel.invokeMethod(OneSignal#pushSubscriptionToken); _id await _channel.invokeMethod(OneSignal#pushSubscriptionId); _optedIn await _channel.invokeMethod(OneSignal#pushSubscriptionOptedIn); return await _channel.invokeMethod(OneSignal#lifecycleInit); }9 个 Channel 各自负责不同功能模块Dart 侧只是一个遥控器——真正的推送注册、令牌获取、用户管理全部由原生侧完成。鸿蒙上没有 OneSignal SDK必须用 hilog 日志桩模拟全部接口。1.3 9 个 Channel 全景序号Channel 名称功能模块Dart 侧文件1OneSignal主通道初始化、登录、授权onesignal_flutter.dart2OneSignal#debug调试日志级别src/debug.dart3OneSignal#inappmessages应用内消息src/inappmessages.dart4OneSignal#liveactivities实时活动iOSsrc/liveactivities.dart5OneSignal#pushsubscription推送订阅状态src/pushsubscription.dart6OneSignal#user用户管理src/user.dart7OneSignal#location位置共享src/location.dart8OneSignal#notifications通知管理src/notifications.dart9OneSignal#session会话追踪src/session.dart1.4 适配策略fork 模式和 in_app_update 一样采用 fork 原仓库 添加 ohos 平台的方式Fork 原仓库保留全部原始文件android/、ios/、lib/、test/ 等不动在 pubspec.yaml 中添加 ohos 平台声明创建 ohos/ 目录作为 HAR 模块编写 ArkTS 原生插件用 flutter create --platforms ohos 生成标准 example 宿主工程在生成的模板上添加插件注册代码为什么不创建独立 OHOS 包fork 模式的好处是原插件的 Dart 层代码lib/完全复用Android/iOS 侧也不受影响一个仓库同时支持三个平台。二、适配流程从 fork 到 ArkTS 原生插件2.1 克隆原仓库cd D:\Flutters git clone --depth 1 https://github.com/OneSignal/OneSignal-Flutter-SDK.git onesignal_flutter_ohos使用 --depth 1 浅克隆因为原仓库历史较大。克隆后保留全部原始文件在原有基础上添加 ohos 相关内容。2.2 修改 pubspec.yaml添加 ohos 平台声明修改前name: onesignal_flutter version: 5.6.10 flutter: plugin: platforms: android: package: com.onesignal.flutter pluginClass: OneSignalPlugin ios: pluginClass: OneSignalPlugin修改后name: onesignal_flutter_ohos version: 5.6.10ohos flutter: plugin: platforms: android: package: com.onesignal.flutter pluginClass: OneSignalPlugin ios: pluginClass: OneSignalPlugin ohos: pluginClass: OneSignalOhosPlugin三个关键改动name 改为 onesignal_flutter_ohos以 _ohos 结尾是社区约定version 追加 ohos 后缀ohos 平台声明 pluginClass: OneSignalOhosPlugin——这就是后面要写的 ArkTS 插件类名2.3 创建 ohos/ HAR 模块在项目根目录创建 ohos/ 目录完整结构如下ohos/ ├── src/main/ets/components/plugin/ │ └── OneSignalOhosPlugin.ets ← ArkTS 原生插件核心实现218 行 ├── src/main/module.json5 ← HAR 模块元信息 ├── index.ets ← HAR 模块入口 ├── oh-package.json5 ← HAR 依赖声明 ├── build-profile.json5 ← 构建配置 ├── hvigorfile.ts ← hvigor 构建脚本 └── BuildProfile.ets ← 构建变量oh-package.json5HAR 依赖声明{ name: onesignal_flutter_ohos, version: 1.0.0, description: HarmonyOS NEXT OneSignal plugin (hilog-based stub), main: index.ets, license: MIT, dependencies: { ohos/flutter_ohos: file:../har } }index.etsHAR 入口导出插件类import OneSignalOhosPlugin from ./src/main/ets/components/plugin/OneSignalOhosPlugin; export default OneSignalOhosPlugin;module.json5HAR 模块元信息{ module: { name: onesignal_flutter_ohos, type: har, description: OneSignal plugin for OpenHarmony, deviceTypes: [phone, tablet, 2in1] } }2.4 编写 ArkTS 原生插件核心这是整个适配的核心文件 OneSignalOhosPlugin.ets。由于鸿蒙没有 OneSignal SDK所有方法采用 hilog 日志桩实现——记录调用日志并返回安全默认值。插件需要实现 FlutterPlugin 和 MethodCallHandler 两个接口在 onAttachedToEngine 中创建全部 9 个 MethodChannelimport { FlutterPlugin, FlutterPluginBinding, MethodCall, MethodCallHandler, MethodChannel, MethodResult, } from ohos/flutter_ohos; import hilog from ohos.hilog; const TAG OneSignal; // All 9 channel names matching Dart side const CHANNEL_MAIN OneSignal; const CHANNEL_DEBUG OneSignal#debug; const CHANNEL_INAPPMESSAGES OneSignal#inappmessages; const CHANNEL_LIVEACTIVITIES OneSignal#liveactivities; const CHANNEL_PUSHSUBSCRIPTION OneSignal#pushsubscription; const CHANNEL_USER OneSignal#user; const CHANNEL_LOCATION OneSignal#location; const CHANNEL_NOTIFICATIONS OneSignal#notifications; const CHANNEL_SESSION OneSignal#session; export default class OneSignalOhosPlugin implements FlutterPlugin, MethodCallHandler { private channels: Mapstring, MethodChannel new Map(); private appId: string ; private loggedIn: boolean false; private logLevel: number 0; private consentRequired: boolean false; private consentGiven: boolean false; private paused: boolean false; private triggers: Recordstring, string {}; private tags: Recordstring, string {}; private aliases: Recordstring, string {}; getUniqueClassName(): string { return OneSignalOhosPlugin; } onAttachedToEngine(binding: FlutterPluginBinding): void { const messenger binding.getBinaryMessenger(); // Create all 9 channels const channelNames [ CHANNEL_MAIN, CHANNEL_DEBUG, CHANNEL_INAPPMESSAGES, CHANNEL_LIVEACTIVITIES, CHANNEL_PUSHSUBSCRIPTION, CHANNEL_USER, CHANNEL_LOCATION, CHANNEL_NOTIFICATIONS, CHANNEL_SESSION ]; for (const name of channelNames) { const ch new MethodChannel(messenger, name); ch.setMethodCallHandler(this); this.channels.set(name, ch); } hilog.info(0x0001, TAG, OneSignal OHOS plugin attached, %{public}d channels registered, channelNames.length); } onDetachedFromEngine(binding: FlutterPluginBinding): void { this.channels.forEach((ch) { ch.setMethodCallHandler(null); }); this.channels.clear(); } onMethodCall(call: MethodCall, result: MethodResult): void { const method call.method; hilog.debug(0x0001, TAG, onMethodCall: %{public}s, method); try { this.routeMethod(method, call, result); } catch (e) { hilog.error(0x0001, TAG, Error handling %{public}s: %{public}s, method, String(e)); result.success(null); } } }routeMethod 方法统一路由所有调用按功能模块分组处理private routeMethod(method: string, call: MethodCall, result: MethodResult): void { // Main channel methods switch (method) { case OneSignal#initialize: this.appId (call.argument(appId) as string) ?? ; hilog.info(0x0001, TAG, Initialized with appId: %{public}s, this.appId); result.success(null); return; case OneSignal#login: this.loggedIn true; hilog.info(0x0001, TAG, Login: %{public}s, call.argument(externalId) as string ?? ); result.success(null); return; case OneSignal#loginWithJWT: this.loggedIn true; result.success(null); return; case OneSignal#logout: this.loggedIn false; result.success(null); return; case OneSignal#consentGiven: this.consentGiven (call.argument(granted) as boolean) ?? false; result.success(null); return; case OneSignal#consentRequired: this.consentRequired (call.argument(required) as boolean) ?? false; result.success(null); return; } // Debug channel methods if (method.startsWith(OneSignal#)) { const sub method.substring(OneSignal#.length); switch (sub) { case setLogLevel: this.logLevel (call.argument(logLevel) as number) ?? 0; result.success(null); return; case getLogLevel: result.success({ console: this.logLevel, visual: this.logLevel }); return; // InAppMessages methods case addTrigger: case addTriggers: case removeTrigger: case removeTriggers: case clearTriggers: case paused: case arePaused: case lifecycleInit: if (sub arePaused) { result.success(this.paused); return; } if (sub paused) { this.paused (call.args as boolean) ?? false; } result.success(null); return; // LiveActivities methods case enterLiveActivity: case exitLiveActivity: case setupDefault: case startDefault: result.success(null); return; // PushSubscription methods case pushSubscriptionToken: result.success(); return; case pushSubscriptionId: result.success(); return; case pushSubscriptionOptedIn: result.success(false); return; case getPermission: result.success({ status: 0, hasPermission: false }); return; case getSubscription: result.success({ id: , token: , enabled: false }); return; // User methods case addAlias: case addAliases: case removeAlias: case removeAliases: case addTag: case addTags: case removeTag: case removeTags: case getTags: result.success(this.tags); return; case addEmail: case removeEmail: case addSms: case removeSms: result.success(null); return; // Location methods case requestPermission: case isShared: case setShared: result.success(sub isShared ? false : null); return; // Notifications methods case permission: result.success(false); return; case permissionNative: result.success(false); return; case canRequest: result.success(true); return; case requestPermission: result.success(false); return; case clearAll: case removeNotification: case removeGroupedNotifications: result.success(null); return; case addForegroundLifecycleListener: case removeForegroundLifecycleListener: case addClickListener: case removeClickListener: case addWillDisplayListener: case removeWillDisplayListener: result.success(null); return; // Session methods case addOutcome: case addUniqueOutcome: case addOutcomes: result.success(null); return; } } // Default: not implemented but return success to avoid crashes hilog.warn(0x0001, TAG, Method not implemented: %{public}s, method); result.success(null); }2.5 关键设计决策解析9 个 Channel 共用一个 Handler和 in_app_update 的 1 个 MethodChannel 1 个 EventChannel 不同OneSignal 有 9 个 MethodChannel。ArkTS 侧用一个 OneSignalOhosPlugin 实例同时处理所有 Channel——所有 Channel 的 setMethodCallHandler 都指向 this然后通过方法名前缀 OneSignal# 统一路由。Channel 名称保持原样const CHANNEL_MAIN OneSignal; const CHANNEL_NOTIFICATIONS OneSignal#notifications; const CHANNEL_PUSHSUBSCRIPTION OneSignal#pushsubscription; // ... 共 9 个所有 Channel 名称与原插件完全一致。这意味着 Dart 侧的 lib/ 目录下所有源码零改动——原插件的 Dart 代码不需要任何修改就能和 ArkTS 原生侧通信。hilog 日志桩而非真实 SDK鸿蒙上没有 OneSignal SDK所以所有方法采用 hilog 日志桩实现记录调用参数返回安全默认值。这不是偷懒——对于插件适配验证来说确保 MethodChannel 通信链路畅通是第一步后续可以逐步替换为真实的推送能力如鸿蒙的 pushKit。返回值类型必须严格匹配 Dart 侧这是本次适配中最关键的发现。Dart 侧的变量类型决定了 ArkTS 侧必须返回对应类型// notifications.dart bool _permission false; // 非空 bool _permission await _channel.invokeMethod(OneSignal#permission);// pushsubscription.dart String? _token; // 可空 String _token await _channel.invokeMethod(OneSignal#pushSubscriptionToken);如果 ArkTS 侧 permission 返回 0int而不是 falseboolDart 侧类型转换直接崩溃。同理 pushSubscriptionToken 返回 null 或空字符串都可以因为 String? 可空但 permission 必须返回 bool。三、example 宿主工程flutter create 生成标准模板3.1 生成 ohos 宿主工程原仓库的 example 目录是 examples/demo/OneSignal 有多个示例应用。在该目录下执行cd D:\Flutters\onesignal_flutter_ohos\examples\demo flutter create --platformsohos .这条命令会在 examples/demo/ 下生成 ohos/ 目录约 39 个文件包含标准的鸿蒙应用模板。注意路径差异OneSignal 原仓库使用 examples/demo/ 而不是常见的 example/所以生成的宿主工程在 examples/demo/ohos/比一般插件多一层目录。3.2 修改 examples/demo/pubspec.yaml原 demo 应用依赖了大量第三方插件shared_preferences、provider、flutter_dotenv、http、url_launcher 等这些插件均未适配 OHOS。直接用会导致启动崩溃。将 pubspec.yaml 精简为最小依赖name: demo description: OneSignal OHOS Test App publish_to: none version: 1.0.01 environment: sdk: ^3.7.0 dependencies: flutter: sdk: flutter onesignal_flutter_ohos: path: ../../ dev_dependencies: flutter_test: sdk: flutter flutter: uses-material-design: true同时替换 lib/main.dart 为最小化测试页面仅依赖 flutter onesignal_flutter_ohos避免未适配插件导致启动崩溃。3.3 修改 import 路径// 原来 import package:onesignal_flutter/onesignal_flutter.dart; // 改为 import package:onesignal_flutter_ohos/onesignal_flutter.dart;lib/ 目录下所有 Dart 文件的内部 import 也需要从 package:onesignal_flutter/ 改为 package:onesignal_flutter_ohos/。3.4 补全 deviceTypes 和权限// examples/demo/ohos/entry/src/main/module.json5 { module: { name: entry, type: entry, deviceTypes: [phone, tablet, 2in1], requestPermissions: [ {name: ohos.permission.INTERNET} ] } }3.5 添加插件注册代码// examples/demo/ohos/entry/src/main/ets/plugins/GeneratedPluginRegistrant.ets import { FlutterEngine, Log } from ohos/flutter_ohos; import OneSignalOhosPlugin from onesignal_flutter_ohos; const TAG GeneratedPluginRegistrant; export class GeneratedPluginRegistrant { static registerWith(flutterEngine: FlutterEngine) { try { flutterEngine.getPlugins()?.add(new OneSignalOhosPlugin()); } catch (e) { Log.e(TAG, Tried to register plugins with FlutterEngine ( flutterEngine ) failed.); Log.e(TAG, Received exception while registering, e); } } }3.6 entry 模块依赖 HAR// examples/demo/ohos/entry/oh-package.json5 { name: entry, version: 1.0.0, dependencies: { onesignal_flutter_ohos: file:../../../../ohos } }注意路径深度由于 demo 在 examples/demo/ 下需要四级 …/ 才能回到项目根目录的 ohos/。四、构建与真机验证4.1 DevEco Studio 配置调试签名DevEco Studio → 文件 → 打开 → 选择 examples/demo/ohos 目录文件 → 项目结构 → 签名配置 → 勾选自动生成签名登录华为账号证书自动填充点确定4.2 执行 flutter pub getcd D:\Flutters\onesignal_flutter_ohos\examples\demo flutter pub get4.3 构建运行在 DevEco Studio 中点运行按钮或命令行构建flutter build hap --release编译输出Dart ArkTS 全部通过仅命令行签名失败 hvigor Finished :entry:defaultFlutterTask... after 16 s 768 ms hvigor Finished :entry:defaultCompileResource... after 355 ms hvigor Finished :entry:defaultBuildJS... after 5 ms4.4 真机效果验证测试页面提供四个按钮覆盖 OneSignal 的核心功能按钮功能调用方法初始化SDK 初始化 lifecycleInitOneSignal.initialize()登录用户登录OneSignal.login()设置标签用户标签管理OneSignal.User.addTags()同意授权隐私授权OneSignal.consentGiven()验证状态汇总验证项状态说明依赖解析flutter pub get通过依赖正常解析Dart 编译通过无编译错误ArkTS 编译通过218 行原生插件编译成功真机运行通过UI 正常显示四个按钮可点击9 个 Channel 注册通过全部 Channel 正常创建initialize lifecycleInit通过初始化链路完整执行Dart ↔ ArkTS 类型匹配通过bool/int/string 返回值类型正确五、常见问题 FAQQ1OneSignal 原仓库有 10 个 Java 文件和 9 个 ChannelArkTS 侧怎么对应ArkTS 侧用一个插件类处理全部 9 个 Channel。关键是在 onAttachedToEngine 中循环创建所有 Channel并统一指向同一个 Handler// ohos/src/main/ets/components/plugin/OneSignalOhosPlugin.ets onAttachedToEngine(binding: FlutterPluginBinding): void { const messenger binding.getBinaryMessenger(); const channelNames [ CHANNEL_MAIN, CHANNEL_DEBUG, CHANNEL_INAPPMESSAGES, CHANNEL_LIVEACTIVITIES, CHANNEL_PUSHSUBSCRIPTION, CHANNEL_USER, CHANNEL_LOCATION, CHANNEL_NOTIFICATIONS, CHANNEL_SESSION ]; for (const name of channelNames) { const ch new MethodChannel(messenger, name); ch.setMethodCallHandler(this); // 所有 Channel 共用一个 Handler this.channels.set(name, ch); } }然后在 routeMethod 中通过方法名前缀统一路由。这比创建 9 个独立 Handler 简洁得多。Q2鸿蒙没有 OneSignal SDK推送功能能用吗当前实现是 hilog 日志桩——所有方法调用会被记录到系统日志并返回安全默认值。推送注册、令牌获取等实际功能需要后续对接鸿蒙的 pushKit华为推送服务来真正实现。适配验证阶段的重点是确保 MethodChannel 通信链路畅通、Dart 侧调用不会崩溃。功能实现可以逐步迭代。Q3初始化时遇到类型转换崩溃怎么办OneSignal 的 initialize() 会级联调用 4 个 lifecycleInit()每个 lifecycleInit 都会通过 MethodChannel 获取返回值。Dart 侧对返回值类型有严格要求// notifications.dart — bool 类型不能返回 int bool _permission false; _permission await _channel.invokeMethod(OneSignal#permission); // pushsubscription.dart — String? 类型可空 String? _token; _token await _channel.invokeMethod(OneSignal#pushSubscriptionToken);ArkTS 侧必须返回完全匹配的类型permission 返回 falsebool而非 0intpushSubscriptionToken 返回 ‘’string而非 null。类型不匹配会直接导致 Dart 侧类型转换异常表现为初始化失败或应用崩溃。Q4原 demo 应用依赖了 shared_preferences 等未适配插件怎么处理OneSignal 原仓库的 examples/demo 依赖了 shared_preferences、provider、flutter_dotenv、http 等第三方插件这些插件均未适配 OHOS。直接运行会因 MissingPluginException 崩溃。解决方案是精简 pubspec.yaml只保留 flutter SDK 和 onesignal_flutter_ohos同时将 main.dart 替换为最小化测试页面。这样可以在不依赖任何未适配插件的情况下验证核心功能。六、总结本文完整记录了 onesignal_flutter 插件从 fork 到鸿蒙 PC 真机验证的全流程采用 fork 模式保留原仓库全部文件用 flutter create --platforms ohos 生成标准宿主工程模板编写 218 行 ArkTS 原生插件代码在一个插件类中处理 9 个 MethodChannel 的 50 方法调用通过 hilog 日志桩模拟 OneSignal SDK 的全部接口。Dart 侧源码零改动所有 Channel 名称与原插件保持一致。核心经验多 Channel 插件的适配关键在于统一路由设计——一个 Handler 处理所有 Channel通过方法名前缀分发比创建多个独立 Handler 更简洁。返回值类型必须严格匹配 Dart 侧的变量类型bool 不能返回 intstring 不能返回 null非空类型时。动手前建议先查一眼 Flutter OH 三方库适配列表很多热门库已有人适配过别重复造轮子。参考资料CPF-Flutter 社区主页CPF-Flutter/flutter_flutterFlutter-OH 主仓库oh-flutter/onesignal_flutter鸿蒙化适配仓库Flutter OH 三方库适配列表OneSignal/OneSignal-Flutter-SDK 原库源码oh-flutter 社区适配参考Flutter 鸿蒙环境搭建指南HarmonyOS pushKit 推送服务文档Flutter OH 官方示例与 FAQ