Flutter + KMP + OpenHarmony 多端项目如何分层:从一个按钮看清调用链
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 负责可共享的业务规则。

本文要解决的四个问题
- Flutter 页面如何避免直接依赖 Android、iOS 或 OpenHarmony API。
- 多平台调用链如何保持同一个方法名和参数结构。
- KMP 共享模块放在哪一层,Android 和 iOS 分别怎么消费。
- 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. 环境边界和当前验证结果
| 项目 | 当前源码信息 |
|---|---|
| Flutter | 3.35.8-ohos-1.0.3-beta |
| Dart | 3.9.2 |
| Android 构建 | AGP 8.9.1、Kotlin 2.3.10 |
| OpenHarmony | compile/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 返回 notImplemented | MainActivity 的方法名 | 对比 getGreeting 是否完全一致 |
| OpenHarmony 构建失败 | ohos/package.json 中的本地 Flutter Hvigor 路径 | 确认 D:\lesson\flutter_flutter 是否存在并可读取 |
| iOS 编译找不到 Shared | AppDelegate.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 的宿主构建记录。
更多推荐



所有评论(0)