Flutter 适配鸿蒙桌面快捷入口完整指南

开源demo地址:https://gitcode.com/nutpi/flutter_openharmony_desktop_quick

给个Star,感谢!!!

目录

  1. 概述
  2. 核心原理
  3. 冷启动实现
  4. 热启动实现
  5. 完整代码示例
  6. 常见问题
  7. 总结

概述

在 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)}`);

总结

关键要点

  1. 冷启动流程:

    • ArkTS 在 onCreate 里存储快捷方式到全局变量
    • Flutter 在 main() 里主动查询 getWantInfo
    • initState 里检查并处理待处理的快捷方式
  2. 热启动流程:

    • ArkTS 在 onNewWant 里直接推送快捷方式
    • Flutter 的 MethodChannel 处理器立即响应
    • 实时跳转,无延迟
  3. 避免重复跳转:

    • 冷启动只存储,不推送
    • 热启动只推送,不存储
    • initState 检查并清空待处理消息
  4. 错误处理:

    • 检查 methodChannel 是否为 null
    • 检查 mounted 确保 widget 已初始化
    • 使用 try-catch 捕获 MethodChannel 异常

测试清单

  • 应用在后台时,长按快捷按钮能立即跳转
  • 杀死应用进程后,长按快捷按钮能冷启动并跳转
  • 快捷方式参数正确传递(PageA / PageB)
  • 没有重复跳转
  • 没有消息丢失
  • 日志输出清晰,便于调试

扩展建议

  1. 支持更多页面:shortcuts_config.json 中添加更多快捷方式
  2. 传递复杂参数: 使用 JSON 序列化在 want.parameters 中传递复杂数据
  3. 动态快捷方式: 使用 OpenHarmony 的动态快捷方式 API 在运行时创建
  4. 性能优化: 减少 Flutter 的初始化延迟,提升冷启动速度
相关推荐
春卷同学1 小时前
足球游戏 - Electron for 鸿蒙PC项目实战案例
游戏·electron·harmonyos
kirk_wang2 小时前
Flutter 三方库鸿蒙适配实践:以 Firebase Messaging 为例实现跨平台推送集成
flutter·移动开发·跨平台·arkts·鸿蒙
春卷同学3 小时前
篮球游戏 - Electron for 鸿蒙PC项目实战案例
游戏·electron·harmonyos
赵财猫._.3 小时前
【Flutter x 鸿蒙】第一篇:环境搭建与第一个鸿蒙Flutter应用运行
flutter·华为·harmonyos
恋猫de小郭3 小时前
Android Studio Otter 2 Feature 发布,最值得更新的 Android Studio
android·前端·flutter
走在路上的菜鸟4 小时前
Android学Dart学习笔记第十二节 函数
android·笔记·学习·flutter
sunly_5 小时前
Flutter:高德定位,获取经纬度,详细地址信息
flutter
解局易否结局5 小时前
Flutter 跨平台开发进阶:从 Widget 思想到全栈集成
flutter
春卷同学5 小时前
滑雪游戏 - Electron for 鸿蒙PC项目实战案例
游戏·electron·harmonyos