新年新气象,小鱼打算跟着【AtomGit 携手开源鸿蒙】开启一段新的旅程:Flutter-OH三方库鸿蒙化。又是一个新的挑战,一个新的从0到1的过程,因为小鱼连flutter插件是什么,怎么工作的都不知道。已经两天的心理建设,不断的告诉自己“慢就是快”,所以小鱼看了一些视频,学了一些基础知识,打算从0出发 🛫,跟着小鱼一起进步吧。

本文两个目标:1、使用MethodChannel调用平台能力:在Flutter页面输入消息内容,点击按钮后调用Notification Kit(用户通知服务)给用户发送一个通知。
2、混合开发场景下,实现ArkUI页面向Flutter页面的跳转与返回,并监听页面的生命周期,适配系统的侧滑手势,实现页面逐级返回
内容实现来源于:开发者学堂 ,文章是我根据自己的理解写的,大家可以边看视频边看文章。

一、基础知识

1、什么是 MethodChannel

简单来说:MethodChannel 是 Flutter 和原生代码(Android/iOS)之间的“电话线”。

  • Flutter 端:你是一个只会说 Dart 语言的人

  • 原生端:对方是一个只会说 Java/Kotlin(Android)、 Objective-C/Swift(iOS)或ArkTs(ohos)的人

  • MethodChannel:就是一部翻译电话,让你们能互相通话

2、为什么需要MethodChannel

Flutter 自己不能做这些事情:

  • 📱 读取电池电量

  • 📍 获取手机位置

  • 📷 调用摄像头

  • 💳 使用指纹/面容识别

  • 🔔 发送本地通知

因为这些功能都是手机系统(原生)才能做的,所以需要通过 MethodChannel 去“打电话”问原生代码要这些信息。

二、实战1:使用MethodChannel调用平台能力

1、Flutter 打电话的人

// 1. 创建一部电话(指定频道名称)
// 这个名字要记住,原生端也要用一样的
  static const platform = MethodChannel('com.zmf.demo');

// 2. 打电话发命令
Future<void> _sendNotification() async {
    String message = _controller.text;
    try {
    // 打电话给原生:我要发送通知
      await platform.invokeMethod('sendANotification', message);
    } on PlatformException catch (e) {
      debugPrint("Failed to send notification: '${e.message}'.");
    }
  }
 

2、ohos 原生端(接电话的人)

接收 Flutter 的调用请求,并利用鸿蒙系统的 NotificationKit 发送通知。
具体实现涉及以下几个步骤:

编写 WorkPlugin 类,实现 FlutterPluginMethodCallHandler 接口,是处理通信的核心类。

  1. 插件初始化
    • onAttachedToEngine 生命周期中,初始化 MethodChannel 并设置当前类为 MethodCallHandler
  onAttachedToEngine(binding: FlutterPluginBinding): void {
    this.channel = new MethodChannel(binding.getBinaryMessenger(), 'com.zmf.demo');
    this.channel.setMethodCallHandler(this);
  }
  1. 消息处理 (onMethodCall)
    • 根据方法名 sendANotification 分发逻辑。
    • 解析 Flutter 传递的参数 message
onMethodCall(call: MethodCall, result: MethodResult): void {
    switch (call.method) {
      case 'sendANotification':
        const message = call.args as string;
        this.sendNotification(message);
        result.success(0);
        break;
      default:
        result.notImplemented();
    }
  }
  1. 发送通知 (sendNotification)
    • 权限检查:首先检查是否已授予通知权限 (notificationManager.isNotificationEnabled)。
    • 权限申请:若未授权,调用 notificationManager.requestEnableNotification 申请权限。
    • 发布通知:构建 NotificationRequest 对象,设置通知标题和内容,调用 notificationManager.publish 发送通知。
sendNotification(message: string) {
    notificationManager.isNotificationEnabled().then((data: boolean) => {
      if (!data) {
        notificationManager.requestEnableNotification(this.context).then(() => {
          console.log(TAG, `requestEnableNotification success`);
          this.send((message));
        }).catch((error: BusinessError) => {
          console.error(TAG, `requestEnableNotification failed, code: ${error.code},  message: ${error.message}`);
        })
      } else {
        this.send(message);
      }
    });
  }

  send(message: string) {
    let notificationRequest: notificationManager.NotificationRequest = {
      id: 1,
      notificationSlotType: notificationManager.SlotType.SOCIAL_COMMUNICATION,
      content: {
        notificationContentType: notificationManager.ContentType.NOTIFICATION_CONTENT_BASIC_TEXT,
        normal: {
          title: '夏小鱼发送的消息通知',
          text: message
        }
      }
    };
    notificationManager.publish(notificationRequest, (error: BusinessError) => {
      if (error) {
        console.error(TAG, `publish notification failed, code: ${error.code},  message: ${error.message}`);
      } else {
        console.log(TAG, `publish notification success`);
      }
    })
  }

3、插件注册

核心功能已经写完了,但是需要在应用的入口 Ability 中注册这个插件,才生效哦。

export default class EntryAbility extends FlutterAbility {
//重写 `configureFlutterEngine` 方法。
  configureFlutterEngine(flutterEngine: FlutterEngine) {
    super.configureFlutterEngine(flutterEngine)
    GeneratedPluginRegistrant.registerWith(flutterEngine)
    // 调用 `flutterEngine.getPlugins().add` 将 `WorkPlugin` 实例添加到引擎中。
    flutterEngine.getPlugins().add(new WorkPlugin(this.context));
  }
}

4、写页面

import 'package:flutter/material.dart';
import 'package:flutter/services.dart';

void main() {
  runApp(const MyApp());
}

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> {
  static const platform = MethodChannel('com.zmf.demo');
  final TextEditingController _controller = TextEditingController();

  Future<void> _sendNotification() async {
    String message = _controller.text;
    try {
      await platform.invokeMethod('sendANotification', message);
    } on PlatformException catch (e) {
      debugPrint("Failed to send notification: '${e.message}'.");
    }
  }

  @override
  void dispose() {
    _controller.dispose();
    super.dispose();
  }

  @override
  Widget build(BuildContext context) {

    return Scaffold(
      appBar: AppBar(
        backgroundColor: Theme.of(context).colorScheme.inversePrimary,
        title: Text(widget.title),
      ),
      body: Center(
        child: Padding(
          padding: const EdgeInsets.symmetric(horizontal: 20.0),
          child: Column(
            mainAxisAlignment: MainAxisAlignment.center,
            children: <Widget>[
              TextField(
                controller: _controller,
                decoration: const InputDecoration(
                  labelText: '通知内容',
                  border: OutlineInputBorder(),
                  contentPadding: EdgeInsets.all(12),
                ),
              ),
              const SizedBox(height: 30),
              ElevatedButton(
                onPressed: _sendNotification,
                style: ElevatedButton.styleFrom(
                  minimumSize: const Size(double.infinity, 48),
                ),
                child: const Text('发送通知'),
              ),
            ],
          ),
        ),
      ),
    );
  }
}

5、流程总结:

  1. 启动应用:Flutter 页面加载,显示输入框和按钮。
  2. 用户操作:用户输入文本 “Hello HarmonyOS”,点击 “发送通知”。
  3. Flutter -> Native:Flutter 通过 com.zmf.demo 通道发送 sendANotification 消息,携带参数 “Hello, HarmonyOS”。
  4. Native 处理
    • WorkPlugin 接收消息。
    • 检查系统通知权限(必要时弹窗申请)。
    • 调用鸿蒙 notificationManager 接口。
  5. 系统响应:系统通知栏显示标题为 “夏小鱼发送的消息通知”,内容为 “Hello, HarmonyOS” 的通知。
    上图:在这里插入图片描述
    在这里插入图片描述

三、实战2:详细介绍如何在 HarmonyOS 应用中集成 Flutter 页面,并实现从 ArkUI 跳转到 Flutter 以及 Flutter 页面间的导航逻辑

1、Flutter 侧页面实现

首先,我们在 Flutter 工程中创建两个页面:Page1Page2Page1 是入口页,包含一个跳转按钮;Page2 是二级页面。

// Page1 (MyHomePage)
class _MyHomePageState extends State<MyHomePage> {
  
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: Text(widget.title)),
      body: Center(
        child: Column(
          children: <Widget>[
            const Text('This is 夏小鱼的 Flutter Page1'),
            ElevatedButton(
              onPressed: () {
                // 使用 Navigator 跳转到 Page2
                Navigator.of(context).push(
                  MaterialPageRoute(builder: (context) => const Page2()),
                );
              },
              child: const Text('push to page2'),
            ),
          ],
        ),
      ),
    );
  }
}

// Page2
class Page2 extends StatelessWidget {
  const Page2({super.key});

  
  Widget build(BuildContext context) {
    return Scaffold(
      body: Center(child: Text('This is 夏小鱼的 Flutter Page2')),
    );
  }
}

2、鸿蒙侧路由跳转 (ArkUI)

在 HarmonyOS 侧,使用 ArkUI 的 Navigation 组件来管理路由。首页是一个 ArkUI 页面,点击按钮跳转到名为 MyFlutterPage 的路由。

@Entry
@Component
struct Index {
  // 创建路由栈
  pathStack: NavPathStack = new NavPathStack()

  build() {
    Navigation(this.pathStack) {
      Column() {
        Button('Push to Flutter Page')
          .onClick(() => {
            // 传递参数,指定 Flutter 初始路由
            let param: Record<string, Object> = { 'route': '/' };
            this.pathStack.pushPathByName('MyFlutterPage', param)
          })
      }
    }
    .navDestination(this.routeMap) // 配置路由映射
  }
}

3、新建 NavFlutterEntry (Flutter 入口封装)

为了让 Flutter 页面能被 ArkUI 的 Navigation 管理,我们需要创建一个继承自 FlutterEntry 的类 NavFlutterEntry。它充当了 Flutter 引擎与当前页面上下文的桥梁。

export class NavFlutterEntry extends FlutterEntry {
  private pathStack: NavPathStack;

  constructor(context: Context, params: Record<string, Object> = {}, pathStack: NavPathStack) {
    super(context, params)
    this.pathStack = pathStack;
  }
}

4、 使用 NavDestination 包裹 FlutterPage

在 ArkUI 的路由目标页面中,我们使用 NavDestination 包裹 FlutterPage 组件。FlutterPage 需要绑定一个 viewId,这个 ID 是由 FlutterEntry 创建的。

@Component
export struct MyFlutterPage {
  // ...
  build() {
    NavDestination() {
      // 绑定 viewId 显示 Flutter 内容
      FlutterPage({ viewId: this.flutterView?.getId() })
    }
    .onReady((context) => {
      this.init(context) // 初始化引擎
    })
    // ... 生命周期绑定
  }
}

5、 初始化 Flutter 引擎

MyFlutterPageinit 方法中,我们初始化 NavFlutterEntry。这里可以通过 route 参数告诉 Flutter 引擎启动时加载哪个页面。

  init(context: NavDestinationContext) {
    this.pathStack = context.pathStack;
    // 获取传递过来的参数 (例如 { 'route': '/' })
    let params: Record<string, Object> = this.pathStack
      .getParamByName('MyFlutterPage')[0] as Record<string, object>
    
    // 初始化 NavFlutterEntry
    this.flutterEntry = new NavFlutterEntry(this.getUIContext().getHostContext()!,
      params, this.pathStack);
      
    this.flutterEntry.aboutToAppear()
    this.flutterView = this.flutterEntry.getFlutterView()
  }

6、 绑定 UIAbility 和 WindowStage

由于 Flutter 是在运行态动态加载的,我们需要在应用启动时(EntryAbility 中)将 UIAbilityWindowStage 绑定到 FlutterManager,确保 Flutter 引擎能正确获取上下文。

export default class EntryAbility extends FlutterAbility {
  onCreate(want: Want, launchParam: AbilityConstant.LaunchParam): void {
    // 绑定 UIAbility
    FlutterManager.getInstance().pushUIAbility(this);
  }

  onWindowStageCreate(windowStage: window.WindowStage): void {
    // 绑定 WindowStage
    FlutterManager.getInstance().pushWindowStage(this, windowStage);
    windowStage.loadContent('pages/Index');
  }

  onDestroy(): void {
    FlutterManager.getInstance().popUIAbility(this);
  }
}

7、 实现逐级返回 (Back Navigation)

为了实现从 Flutter 页面 2 返回 页面 1,再返回 ArkUI 页面,我们需要处理两个关键点:

  1. NavDestination.onBackPressed: 拦截系统返回键(包括侧滑手势)。
    • 通过 eventHub 发送事件
    • 调用 flutterEntry.onBackPress() 通知 Flutter 引擎处理内部路由回退。
    .onBackPressed(() => { 
      console.log('[MyFlutterPage] onBackPressed()')
      this.context.eventHub.emit('EVENT_BACK_PRESS')
      // 关键:通知 Flutter 处理返回
      this.flutterEntry?.onBackPress()
      return true // 返回 true 表示我们消费了该事件,不再由系统默认处理
    })
  1. NavFlutterEntry.popSystemNavigator: 当 Flutter 路由栈为空时(即在 Flutter 首页按返回),Flutter 会调用此方法请求退出。此时我们调用 ArkUI 的 pathStack.pop() 返回上一级 ArkUI 页面。
  popSystemNavigator(): boolean {
    if (this.pathStack) {
      // Flutter 页面已退完,现在退出 ArkUI 的 NavDestination
      this.pathStack.pop();
      return true;
    }
    return false;
  }

8、上图

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

今天结束啦,又是上班摸鱼,收获满满的一天。如果需要代码给我留言~

Logo

作为“人工智能6S店”的官方数字引擎,为AI开发者与企业提供一个覆盖软硬件全栈、一站式门户。

更多推荐