Flutter + KMP + OpenHarmony 多端项目如何分层:从一个按钮看清调用链

开篇:一个按钮为什么值得单独拆开

在多端项目里,最容易被低估的是一个看似简单的“调用共享模块”按钮。Flutter 页面负责输入和状态展示,Dart 需要判断当前平台,Android 或 iOS 再把请求交给 Kotlin Multiplatform,OpenHarmony、Web 和 Windows 还要使用自己的回退实现。如果这些职责混在页面里,功能一多就会出现平台判断散落、错误信息丢失和测试无法替换真实平台的问题。

本文以一个 Flutter + Kotlin Multiplatform(KMP)+ OpenHarmony 的多端示例项目为例。项目源码目录名为 lesson59,后文用 lesson59 指代这个示例项目。文章沿着一次问候请求,把 Flutter、MethodChannel、KMP shared、iOS 框架和 OpenHarmony Flutter 宿主串起来。项目当前的业务很小,正适合把边界看清楚:页面只处理交互,Gateway 负责平台选择,原生层负责桥接,KMP 负责可共享的业务规则。

在这里插入图片描述

本文要解决的四个问题

  1. Flutter 页面如何避免直接依赖 Android、iOS 或 OpenHarmony API。
  2. 多平台调用链如何保持同一个方法名和参数结构。
  3. KMP 共享模块放在哪一层,Android 和 iOS 分别怎么消费。
  4. OpenHarmony 没有接入 KMP 时,如何保留可运行的 Dart 回退能力。

1. 先从项目目录确认职责

项目根目录的 README 已经给出了一条清晰的分层关系:lib/app 管入口与路由,lib/core 放跨功能基础能力,lib/features 放业务页面,shared 放 Android 和 iOS 共用的 KMP 逻辑。

本次调用的实际路径如下:

在这里插入图片描述

HomePage
  -> GreetingGateway.greet(name)
  -> Android/iOS: MethodChannel
  -> KMP shared: GreetingService.greet(name)
  -> String result

HomePage
  -> GreetingGateway.greet(name)
  -> OpenHarmony/Web/Windows: LocalGreetingGateway
  -> String result

这里的关键不是目录名字,而是每个边界只承担一种责任。页面不需要知道 KMP 类名,KMP 也不应该知道 Flutter 的控件状态。

2. 入口只负责启动应用

main.dart 做了两件事:确保 Flutter 绑定初始化,然后交给 App 组件。它没有在入口里判断平台,也没有创建原生通道。

import 'package:flutter/widgets.dart';

import 'app/app.dart';

void main() {
  WidgetsFlutterBinding.ensureInitialized();
  runApp(const App());
}

这段代码的边界是应用启动。输入是 Flutter 运行时,输出是根组件;它避免了把平台能力初始化塞进 main,后续页面测试可以直接构造 HomePage,不必启动完整应用。

App 只配置主题、路由和首屏:

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

  
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Lesson 59',
      debugShowCheckedModeBanner: false,
      theme: AppTheme.light,
      darkTheme: AppTheme.dark,
      themeMode: ThemeMode.system,
      initialRoute: AppRoutes.home,
      routes: {AppRoutes.home: (_) => const HomePage()},
    );
  }
}

主题和路由在这里完成装配,业务页面只接收自己的依赖。 ThemeMode.system 让系统深浅色设置自然进入 Flutter 层,路由表则保留了以后增加页面的入口。对应源码文件是 lib/app/app.dart。

3. 页面通过接口拿能力

页面构造函数允许传入 GreetingGateway:

class HomePage extends StatefulWidget {
  const HomePage({super.key, this.greetingGateway});

  final GreetingGateway? greetingGateway;

  
  State<HomePage> createState() => _HomePageState();
}

当应用真正运行时,没有传入的依赖会在 initState 中自动创建;测试时可以注入假实现。这让页面依赖“能问候的能力”,而不是依赖 MethodChannel 或具体平台。对应源码文件是 lib/features/home/presentation/home_page.dart。


void initState() {
  super.initState();
  _greetingGateway = widget.greetingGateway ?? createGreetingGateway();
}

这里的输入是可选的 Gateway,输出是页面内部保存的唯一调用对象。边界明确后,平台选择逻辑不会和控件树互相污染。

4. Gateway 统一平台选择

平台判断集中在 createGreetingGateway:

GreetingGateway createGreetingGateway() {
  final usesKmp =
      !kIsWeb &&
      (defaultTargetPlatform == TargetPlatform.android ||
          defaultTargetPlatform == TargetPlatform.iOS);
  return usesKmp
      ? const MethodChannelGreetingGateway()
      : const LocalGreetingGateway();
}

它信任的输入是 Flutter 的平台标识和 kIsWeb。返回结果只有两类:Android/iOS 使用通道实现,其他平台使用本地实现。这样页面只调用 greet,不会出现平台分支写在按钮和异常处理里的情况。

在这里插入图片描述

5. Android 与 iOS 共享同一个通道契约

Dart 侧固定使用通道名 com.example.lesson59/kmp 和方法名 getGreeting:

static const _channel = MethodChannel('com.example.lesson59/kmp');


Future<String> greet(String name) async {
  final result = await _channel.invokeMethod<String>('getGreeting', {
    'name': name,
  });
  if (result == null) {
    throw StateError('KMP returned an empty result.');
  }
  return result;
}

通道层只关心协议,不负责修剪名称或决定业务文案。参数校验由 KMP 统一完成,结果为空时由 Dart 转成状态错误,避免页面收到一个无法展示的空字符串。对应源码文件是 lib/core/platform/greeting_gateway.dart。

Android 的 MainActivity 和 iOS 的 AppDelegate 都接收相同的方法名与参数。这个约定让 Flutter 页面只拥有一套调用代码,平台差异收敛在宿主入口。

6. OpenHarmony 的宿主只负责承载 Flutter

OpenHarmony 的 EntryAbility 继承 FlutterAbility,在配置引擎时注册插件:

import { FlutterAbility, FlutterEngine } from '@ohos/flutter_ohos';
import { GeneratedPluginRegistrant } from '../plugins/GeneratedPluginRegistrant';

export default class EntryAbility extends FlutterAbility {
  configureFlutterEngine(flutterEngine: FlutterEngine) {
    super.configureFlutterEngine(flutterEngine)
    GeneratedPluginRegistrant.registerWith(flutterEngine)
  }
}

页面入口 Index.ets 使用 FlutterPage 承载 Flutter 视图,并把返回事件通过 eventHub 发出去。ArkTS 层不重复实现业务页面,Flutter 仍然是 UI 主体。对应源码文件是 ohos/entry/src/main/ets/entryability/EntryAbility.ets 和 ohos/entry/src/main/ets/pages/Index.ets。

7. 环境边界和当前验证结果

项目当前源码信息
Flutter3.35.8-ohos-1.0.3-beta
Dart3.9.2
Android 构建AGP 8.9.1、Kotlin 2.3.10
OpenHarmonycompile/target/compatible SDK 20
Flutter 依赖Flutter SDK、flutter_test、flutter_lints 5.0.0
当前业务输入名称后调用共享问候逻辑

本地验证命令:

flutter pub get
flutter analyze
flutter test

本轮结果为:flutter analyze passed,flutter test passed。Android 真机、OpenHarmony 真机和 iOS XCFramework 构建仍需要对应设备或 macOS 环境。

8. 多端架构的故障排查

现象优先检查处理方向
页面启动但调用按钮没有结果createGreetingGateway 的平台分支先打印当前平台,再确认是否进入本地回退或 MethodChannel
Android 返回 notImplementedMainActivity 的方法名对比 getGreeting 是否完全一致
OpenHarmony 构建失败ohos/package.json 中的本地 Flutter Hvigor 路径确认 D:\lesson\flutter_flutter 是否存在并可读取
iOS 编译找不到 SharedAppDelegate.swift 的条件编译和框架链接先生成 Shared.xcframework,再配置 Runner
空名称时页面显示原生异常KMP 的参数校验和 Dart 的异常展示保留错误码与用户可读消息,不吞掉异常

9. 多端架构的验收清单

  • Flutter 页面没有直接引用 Android、iOS 或 OHOS 平台类。
  • 通道名和方法名在 Dart、Android、iOS 三端一致。
  • Android 与 iOS 的共享业务规则来自 shared。
  • Web、Windows 和 OpenHarmony 仍能走 Dart 回退实现。
  • 空名称能够进入明确的错误分支。
  • flutter analyze 和 flutter test 已通过。
  • 真机验证结果单独记录,并注明设备与系统版本。

把边界留给下一次扩展

这个项目的价值在于把多端协作压缩成一条可追踪链路:Flutter 负责交互,Gateway 负责能力选择,MethodChannel 负责协议传输,KMP 负责共享业务规则,OpenHarmony 宿主负责 Flutter 生命周期和页面承载。以后增加真实业务时,只要沿着这条边界扩展,就能减少平台判断和页面逻辑互相穿插的问题。

文章中的“已通过”和“未运行”必须按平台分别记录。Flutter 层测试通过,只能说明 Dart 页面和测试用例稳定;它不能替代 Android、iOS 或 OpenHarmony 的宿主构建记录。

Logo

开源鸿蒙跨平台开发社区汇聚开发者与厂商,共建“一次开发,多端部署”的开源生态,致力于降低跨端开发门槛,推动万物智联创新。

更多推荐