开源demo地址:https://gitcode.com/nutpi/flutter_openharmony_desktop_quick
给个Star,感谢!!!
目录
概述
在 OpenHarmony(鸿蒙)系统中,用户可以通过长按桌面快捷按钮快速启动应用的特定页面。本文详细记录了如何在 Flutter 混合应用中正确实现这一功能,包括冷启动(应用被杀死)和热启动(应用在后台)两种场景。
关键挑战:
- 冷启动时 Flutter Engine 还未初始化,无法直接通过 MethodChannel 通信
- 热启动时需要实时推送快捷方式信息给 Flutter
- 需要避免重复跳转和消息丢失
核心原理
系统架构
┌─────────────────────────────────────────────────────┐
│ OpenHarmony 系统 │
│ │
│ ┌───────────────────────────────────────────────┐ │
│ │ EntryAbility (ArkTS) │ │
│ │ - 接收桌面快捷方式的 Want 对象 │ │
│ │ - 通过 MethodChannel 与 Flutter 通信 │ │
│ │ - 管理全局快捷方式存储 │ │
│ └───────────────────────────────────────────────┘ │
│ ↕ │
│ MethodChannel 通道 │
│ ↕ │
│ ┌───────────────────────────────────────────────┐ │
│ │ Flutter Engine (Dart) │ │
│ │ - 接收快捷方式信息 │ │
│ │ - 通过 ValueNotifier 触发导航 │ │
│ │ - 执行 Navigator.push 跳转 │ │
│ └───────────────────────────────────────────────┘ │
│ │
└─────────────────────────────────────────────────────┘
关键概念
Want 对象: OpenHarmony 中用于传递意图和参数的对象
- 包含
parameters字段,存放快捷方式参数 - 示例:
want.parameters['shortCutKey'] = 'PageA'
MethodChannel: Flutter 与原生代码的双向通信通道
- 通道名称必须 ArkTS 和 Flutter 两端一致
- 支持 Flutter → ArkTS 和 ArkTS → Flutter 两个方向
全局变量 pendingShortcutKey: 进程级别的快捷方式存储
- 在 ArkTS 顶部定义,生命周期为整个进程
- 用于冷启动时暂存快捷方式信息
冷启动实现
场景描述
用户杀死应用进程后,长按桌面快捷按钮启动应用。此时 Flutter Engine 还未初始化。
时序流程
1. 系统启动 EntryAbility
↓
2. 调用 onCreate(want, launchParam)
- want.parameters = { shortCutKey: 'PageA' }
↓
3. ArkTS 处理快捷方式
- 检测到冷启动(isColdStart = true)
- 只存储到全局变量 pendingShortcutKey
- 不直接推送(因为 Flutter Engine 还没起来)
↓
4. configureFlutterEngine() 执行
- 初始化 MethodChannel
- 注册处理器
↓
5. Flutter main() 执行
- 注册 MethodChannel 处理器
- 延迟 500ms 等待 UI 初始化
- 调用 channel.invokeMethod('getWantInfo')
↓
6. ArkTS 的 getWantInfo 处理器
- 读取全局变量 pendingShortcutKey = 'PageA'
- 返回给 Flutter
- 清空全局变量
↓
7. Flutter 更新 shortcutNotifier.value = 'PageA'
↓
8. MyHomePage.initState() 检测到待处理快捷方式
- 调用 _onShortcutChanged()
↓
9. Navigator.push(PageA)
↓
10. 用户看到 PageA 页面
ArkTS 冷启动代码
typescript
// 全局变量
let pendingShortcutKey: string | null = null;
export default class EntryAbility extends FlutterAbility {
private methodChannel: MethodChannel | null = null;
// 冷启动时系统调用
onCreate(want: Want, launchParam: AbilityConstant.LaunchParam): void {
super.onCreate(want, launchParam);
console.log('========== onCreate 被调用(冷启动)==========');
this.processShortcut(want, true); // isColdStart = true
}
private processShortcut(want: Want, isColdStart: boolean): void {
if (want.parameters && want.parameters['shortCutKey']) {
const shortcutKey = want.parameters['shortCutKey'] as string;
if (isColdStart) {
// 冷启动:只存到全局变量,让 Flutter 主动查询
pendingShortcutKey = shortcutKey;
console.log(`✓ 冷启动:已存储到全局变量 pendingShortcutKey: ${pendingShortcutKey}`);
}
}
}
// getWantInfo 处理器
// 在 setMethodCallHandler 中实现
if (call.method === 'getWantInfo') {
if (pendingShortcutKey) {
const shortcutKey = pendingShortcutKey;
console.log(`✓ 返回存储的快捷方式信息:${shortcutKey}`);
pendingShortcutKey = null; // 用完就清空
result.success(shortcutKey);
} else {
result.success(null);
}
}
}
Flutter 冷启动代码
dart
void main() async {
debugPrint('========== Flutter main() 开始 ==========');
runApp(const MyApp());
// 注册 MethodChannel 处理器
channel.setMethodCallHandler((MethodCall call) async {
if (call.method == 'navigateToPage') {
final String shortcutKey = call.arguments as String;
debugPrint('✓ 收到 ArkTS 快捷方式:$shortcutKey');
shortcutNotifier.value = shortcutKey;
}
});
// 延迟等待 Flutter UI 初始化
await Future.delayed(const Duration(milliseconds: 500));
try {
// 主动查询 ArkTS 存储的快捷方式
final result = await channel.invokeMethod('getWantInfo');
if (result != null) {
debugPrint('✓ 从 ArkTS 获取到快捷方式:$result');
shortcutNotifier.value = result as String;
}
} on PlatformException catch (e) {
debugPrint('✗ 获取快捷方式失败:${e.message}');
}
}
Flutter 冷启动导航代码
dart
class _MyHomePageState extends State<MyHomePage> {
@override
void initState() {
super.initState();
shortcutNotifier.addListener(_onShortcutChanged);
// 关键:检查是否已有待处理的快捷方式
// (在监听器添加前就被 ArkTS 推送了)
if (shortcutNotifier.value != null) {
debugPrint('initState 检测到已有待处理的快捷方式:${shortcutNotifier.value}');
Future.microtask(() => _onShortcutChanged());
}
}
void _onShortcutChanged() {
final shortcutKey = shortcutNotifier.value;
if (shortcutKey == null || !mounted) return;
shortcutNotifier.value = null; // 清空,避免重复触发
if (shortcutKey == 'PageA') {
Navigator.push(
context,
MaterialPageRoute(builder: (_) => const PageA()),
);
} else if (shortcutKey == 'PageB') {
Navigator.push(
context,
MaterialPageRoute(builder: (_) => const PageB()),
);
}
}
@override
void dispose() {
shortcutNotifier.removeListener(_onShortcutChanged);
super.dispose();
}
}
热启动实现
场景描述
应用在后台存活时,用户长按桌面快捷按钮。此时 Flutter Engine 已经初始化,可以直接通信。
时序流程
1. 用户长按桌面快捷按钮
↓
2. 系统调用 EntryAbility.onNewWant(want)
- want.parameters = { shortCutKey: 'PageA' }
↓
3. ArkTS 处理快捷方式
- 检测到热启动(isColdStart = false)
- methodChannel 已初始化
- 直接调用 invokeMethod('navigateToPage', 'PageA')
↓
4. Flutter 的 MethodChannel 处理器立即触发
- 更新 shortcutNotifier.value = 'PageA'
↓
5. _onShortcutChanged() 被触发
↓
6. Navigator.push(PageA)
↓
7. 用户立即看到 PageA 页面(无延迟)
ArkTS 热启动代码
typescript
// 热启动时系统调用
onNewWant(want: Want): void {
console.log('========== onNewWant 被调用(热启动)==========');
this.processShortcut(want, false); // isColdStart = false
}
private processShortcut(want: Want, isColdStart: boolean): void {
if (want.parameters && want.parameters['shortCutKey']) {
const shortcutKey = want.parameters['shortCutKey'] as string;
if (!isColdStart) {
// 热启动:直接推送给 Flutter
if (this.methodChannel) {
console.log(`✓ 热启动:准备调用 Flutter navigateToPage`);
try {
this.methodChannel.invokeMethod('navigateToPage', shortcutKey);
console.log(`✓ invokeMethod 调用成功`);
} catch (e) {
console.log(`✗ invokeMethod 调用失败: ${e}`);
}
} else {
// 备选方案:methodChannel 还没初始化
console.log(`✗ 热启动但 methodChannel 为 null,存到全局变量`);
pendingShortcutKey = shortcutKey;
}
}
}
}
热启动的优势
- 实时响应: 无需等待 Flutter 初始化,直接推送
- 用户体验好: 点击快捷方式立即跳转,无延迟
- 简单高效: 直接通过 MethodChannel 通信
完整代码示例
1. 快捷方式配置文件
ohos/entry/src/main/resources/base/profile/shortcuts_config.json
json
{
"shortcuts": [
{
"startingIntent": {
"targetBundle": "com.lefushuju.mojiqian",
"targetClass": "com.lefushuju.mojiqian.EntryAbility"
},
"icon": "$media:icon",
"id": "id_page_a",
"label": "$string:page_a_label",
"parameters": {
"shortCutKey": "PageA"
}
},
{
"startingIntent": {
"targetBundle": "com.lefushuju.mojiqian",
"targetClass": "com.lefushuju.mojiqian.EntryAbility"
},
"icon": "$media:icon",
"id": "id_page_b",
"label": "$string:page_b_label",
"parameters": {
"shortCutKey": "PageB"
}
}
]
}
2. module.json5 配置
json5
{
"module": {
"name": "entry",
"type": "entry",
"abilities": [
{
"name": "EntryAbility",
"srcEntry": "./ets/entryability/EntryAbility.ets",
"description": "$string:EntryAbility_desc",
"icon": "$media:icon",
"label": "$string:EntryAbility_label",
"startWindowIcon": "$media:icon",
"startWindowBackground": "$color:start_window_background",
"exported": true,
"metadata": [
{
"name": "ohos.ability.shortcuts",
"resource": "$profile:shortcuts_config"
}
]
}
],
"requestPermissions": []
}
}
3. 完整的 EntryAbility.ets
typescript
import { FlutterAbility, FlutterEngine, MethodChannel, MethodCall, MethodResult } from '@ohos/flutter_ohos';
import { GeneratedPluginRegistrant } from '../plugins/GeneratedPluginRegistrant';
import Want from '@ohos.app.ability.Want';
import AbilityConstant from '@ohos.app.ability.AbilityConstant';
// 全局变量,存储快捷方式信息(进程级别共享)
let pendingShortcutKey: string | null = null;
export default class EntryAbility extends FlutterAbility {
private methodChannel: MethodChannel | null = null;
configureFlutterEngine(flutterEngine: FlutterEngine): void {
super.configureFlutterEngine(flutterEngine)
GeneratedPluginRegistrant.registerWith(flutterEngine)
// 创建MethodChannel实例,通道名称必须与Flutter端一致
this.methodChannel = new MethodChannel(flutterEngine.dartExecutor, 'com.lefushuju.mojiqian');
// 设置方法调用处理器
this.methodChannel.setMethodCallHandler({
onMethodCall: async (call: MethodCall, result: MethodResult): Promise<void> => {
if (call.method === 'sendToArkTS') {
// 类型安全的参数获取
const data: string | number | boolean | object | null = call.args;
const dataString: string = typeof data === 'string' ? data : JSON.stringify(data);
console.log(`收到Flutter数据:${dataString}`);
// 返回处理结果给Flutter
result.success(`ArkTS已收到:${dataString}`);
} else if (call.method === 'getWantInfo') {
// Flutter 查询是否有存储的快捷方式信息
console.log(`getWantInfo 被调用,pendingShortcutKey: ${pendingShortcutKey}`);
if (pendingShortcutKey) {
const shortcutKey = pendingShortcutKey;
console.log(`✓ 返回存储的快捷方式信息:${shortcutKey}`);
pendingShortcutKey = null; // 用完就清空
result.success(shortcutKey);
} else {
console.log(`✗ pendingShortcutKey 为空`);
result.success(null);
}
} else {
// 处理未知方法调用
result.error('404', '方法未找到', null);
}
}
})
}
// 冷启动时,系统首先调用 onCreate,并把桌面快捷方式的 Want 传进来
onCreate(want: Want, launchParam: AbilityConstant.LaunchParam): void {
super.onCreate(want, launchParam);
console.log('========== onCreate 被调用(冷启动)==========');
this.processShortcut(want, true); // 传入 isColdStart = true
}
// 当能力已在内存中,再次通过快捷方式拉起时会调用 onNewWant
onNewWant(want: Want): void {
console.log('========== onNewWant 被调用(热启动)==========');
this.processShortcut(want, false); // 传入 isColdStart = false
}
// 提取公共的快捷方式处理逻辑
private processShortcut(want: Want, isColdStart: boolean): void {
console.log(`want.parameters: ${JSON.stringify(want.parameters)}`);
console.log(`want.parameters 类型: ${typeof want.parameters}`);
console.log(`isColdStart: ${isColdStart}`);
// 处理快捷方式
if (want.parameters) {
console.log(`want.parameters 的所有 key: ${Object.keys(want.parameters)}`);
// 尝试多种参数名
let shortcutKey: string | null = null;
if (want.parameters['shortCutKey']) {
shortcutKey = want.parameters['shortCutKey'] as string;
console.log(`✓ 从 shortCutKey 获取到: ${shortcutKey}`);
} else if (want.parameters['shortcutKey']) {
shortcutKey = want.parameters['shortcutKey'] as string;
console.log(`✓ 从 shortcutKey 获取到: ${shortcutKey}`);
} else {
console.log(`✗ want.parameters 中没有 shortCutKey 或 shortcutKey`);
}
if (shortcutKey) {
console.log(`========== 收到快捷方式:${shortcutKey} ==========`);
if (isColdStart) {
// 冷启动:只存到全局变量,让 Flutter 主动查询
pendingShortcutKey = shortcutKey;
console.log(`✓ 冷启动:已存储到全局变量 pendingShortcutKey: ${pendingShortcutKey}`);
console.log(`✓ 等待 Flutter 启动后调用 getWantInfo`);
} else {
// 热启动:直接推送给 Flutter
if (this.methodChannel) {
console.log(`✓ 热启动:methodChannel 已初始化,准备调用 Flutter navigateToPage`);
console.log(`✓ 调用 invokeMethod('navigateToPage', '${shortcutKey}')`);
try {
this.methodChannel.invokeMethod('navigateToPage', shortcutKey);
console.log(`✓ invokeMethod 调用成功`);
} catch (e) {
console.log(`✗ invokeMethod 调用失败: ${e}`);
}
} else {
console.log(`✗ 热启动但 methodChannel 为 null,存到全局变量`);
pendingShortcutKey = shortcutKey;
}
}
}
} else {
console.log(`✗ want.parameters 为空或 undefined`);
}
console.log('========== processShortcut 结束 ==========');
}
}
4. 完整的 main.dart
dart
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'pages/page_a.dart';
import 'pages/page_b.dart';
final ValueNotifier<String?> shortcutNotifier = ValueNotifier<String?>(null);
const channel = MethodChannel('com.lefushuju.mojiqian');
void main() async {
debugPrint('========== Flutter main() 开始 ==========');
runApp(const MyApp());
channel.setMethodCallHandler((MethodCall call) async {
if (call.method == 'navigateToPage') {
final String shortcutKey = call.arguments as String;
debugPrint('✓ 收到 ArkTS 快捷方式:$shortcutKey');
shortcutNotifier.value = shortcutKey;
}
});
await Future.delayed(const Duration(milliseconds: 500));
try {
final result = await channel.invokeMethod('getWantInfo');
if (result != null) {
debugPrint('✓ 从 ArkTS 获取到快捷方式:$result');
shortcutNotifier.value = result as String;
}
} on PlatformException catch (e) {
debugPrint('✗ 获取快捷方式失败:${e.message}');
}
debugPrint('========== Flutter main() 结束 ==========');
}
class MyApp extends StatelessWidget {
const MyApp({super.key});
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Flutter Demo',
theme: ThemeData(colorScheme: ColorScheme.fromSeed(seedColor: Colors.deepPurple)),
home: const MyHomePage(title: 'Flutter Demo Home Page'),
);
}
}
class MyHomePage extends StatefulWidget {
const MyHomePage({super.key, required this.title});
final String title;
@override
State<MyHomePage> createState() => _MyHomePageState();
}
class _MyHomePageState extends State<MyHomePage> {
int _counter = 0;
@override
void initState() {
super.initState();
shortcutNotifier.addListener(_onShortcutChanged);
// 关键:检查是否已有待处理的快捷方式
if (shortcutNotifier.value != null) {
debugPrint('initState 检测到已有待处理的快捷方式:${shortcutNotifier.value}');
Future.microtask(() => _onShortcutChanged());
}
}
void _onShortcutChanged() {
debugPrint('========== _onShortcutChanged 被触发 ==========');
final shortcutKey = shortcutNotifier.value;
debugPrint('shortcutKey: $shortcutKey');
if (shortcutKey == null) {
debugPrint('✗ shortcutKey 为 null,返回');
return;
}
if (!mounted) {
debugPrint('✗ widget 未挂载,返回');
return;
}
shortcutNotifier.value = null;
debugPrint('✓ 已清空 shortcutNotifier.value');
if (shortcutKey == 'PageA') {
debugPrint('✓ 准备导航到 PageA');
Navigator.push(
context,
MaterialPageRoute(builder: (_) => const PageA()),
);
} else if (shortcutKey == 'PageB') {
debugPrint('✓ 准备导航到 PageB');
Navigator.push(
context,
MaterialPageRoute(builder: (_) => const PageB()),
);
}
}
@override
void dispose() {
shortcutNotifier.removeListener(_onShortcutChanged);
super.dispose();
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text(widget.title),
),
body: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
ElevatedButton(
onPressed: () {
Navigator.push(
context,
MaterialPageRoute(builder: (context) => const PageA()),
);
},
child: const Text('Go to Page A'),
),
const SizedBox(height: 10),
ElevatedButton(
onPressed: () {
Navigator.push(
context,
MaterialPageRoute(builder: (context) => const PageB()),
);
},
child: const Text('Go to Page B'),
),
],
),
),
);
}
}
常见问题
Q1: 为什么冷启动和热启动要区分处理?
A:
- 冷启动时 Flutter Engine 还未初始化,无法通过 MethodChannel 直接通信
- 热启动时 Flutter Engine 已准备好,可以实时推送消息
- 区分处理能避免重复跳转和消息丢失
Q2: 为什么要在 initState 里检查 shortcutNotifier.value?
A:
- 时序问题:ArkTS 可能在 Flutter 的监听器添加前就推送了消息
- ValueNotifier 只在值变化时触发监听器,不会重复触发
- 在
initState检查能捕获这些"提前到达"的消息
Q3: 为什么要用全局变量而不是 AppStorage?
A:
- AppStorage 是 ArkUI 框架的状态容器,在 Flutter Hybrid 应用中不可靠
- 全局变量是进程级别的,生命周期清晰
- 避免了跨框架状态管理的复杂性
Q4: MethodChannel 的通道名称有什么要求?
A:
- 必须 ArkTS 和 Flutter 两端完全一致
- 建议使用反向域名格式,如
com.lefushuju.mojiqian - 不能为空或包含特殊字符
Q5: 如何调试快捷方式功能?
A:
bash
# 查看 ArkTS 日志
hdc shell hilog -L LEVEL_INFO -T kuaisurukou
# 查看 Flutter 日志
flutter run -v
# 查看完整的 want.parameters
console.log(`want.parameters: ${JSON.stringify(want.parameters)}`);
总结
关键要点
-
冷启动流程:
- ArkTS 在
onCreate里存储快捷方式到全局变量 - Flutter 在
main()里主动查询getWantInfo - 在
initState里检查并处理待处理的快捷方式
- ArkTS 在
-
热启动流程:
- ArkTS 在
onNewWant里直接推送快捷方式 - Flutter 的 MethodChannel 处理器立即响应
- 实时跳转,无延迟
- ArkTS 在
-
避免重复跳转:
- 冷启动只存储,不推送
- 热启动只推送,不存储
- 在
initState检查并清空待处理消息
-
错误处理:
- 检查
methodChannel是否为 null - 检查
mounted确保 widget 已初始化 - 使用 try-catch 捕获 MethodChannel 异常
- 检查
测试清单
- 应用在后台时,长按快捷按钮能立即跳转
- 杀死应用进程后,长按快捷按钮能冷启动并跳转
- 快捷方式参数正确传递(PageA / PageB)
- 没有重复跳转
- 没有消息丢失
- 日志输出清晰,便于调试
扩展建议
- 支持更多页面: 在
shortcuts_config.json中添加更多快捷方式 - 传递复杂参数: 使用 JSON 序列化在
want.parameters中传递复杂数据 - 动态快捷方式: 使用 OpenHarmony 的动态快捷方式 API 在运行时创建
- 性能优化: 减少 Flutter 的初始化延迟,提升冷启动速度