> 技术文档 > Flutter 与HarmonyOS Next 混合渲染开发实践:以 fluttertpc_scan 三方库为例_flutter与鸿蒙混合开发

Flutter 与HarmonyOS Next 混合渲染开发实践:以 fluttertpc_scan 三方库为例_flutter与鸿蒙混合开发


一、背景与价值

在跨平台开发中,Flutter 以其高效的 UI 构建能力著称,而鸿蒙 Next(OpenHarmony)则提供了深度系统集成的原生能力。将两者结合,可实现 UI 跨平台 + 原生功能深度融合 的混合渲染模式。本文以扫描库 fluttertpc_scan 为例,详解混合开发的核心流程。


二、环境配置(关键细节优化)

1. 工具链版本要求

工具 最低版本 备注 Flutter SDK 3.19.0+ 支持ohos的FlutterSDK DevEco Studio 5.0.1 Release 需配置 OpenHarmony 5.0+ SDK

2. 环境验证

# 验证 Flutter 环境flutter doctor -v# 确认鸿蒙 SDK 路径# DevEco Studio → File → Settings → SDK Manager → OpenHarmony SDK

三、混合渲染实现(代码深度优化)

1. 鸿蒙原生模块开发

1.1 二维码扫描组件封装
@Componentexport struct CustomScanPage { @StorageLink(CameraConstants.CUSTOM_SCAN_PAGE_IS_BACKGROUND_NAME) @Watch(\'onBackgroundUpdate\') isBackground: boolean = false; @State params: Map<String, Object> | null = null @State customScanVM: CustomScanViewModel = CustomScanViewModel.getInstance(); private mXComponentController: XComponentController = new XComponentController(); @State scanLineColor: string = \"#ff4caf50\" private mScale: number = 1.0 @State animationOrdinate: number = CameraConstants.SCAN_DIVIDER_OFFSET_BEGIN @State pauseScan: boolean = false args?: Params build() { Column() { Stack() { XComponent({ id: CameraConstants.CAMERA_COMPONENT_ID, type: CameraConstants.CAMERA_COMPONENT_TYPE, controller: this.mXComponentController }) .onLoad(() => { Log.i(TAG, \'XComponent onLoad\') this.customScanStart() }) .width(this.customScanVM.cameraCompWidth) .height(this.customScanVM.cameraCompHeight) .position({ x: CameraConstants.SCAN_COMPONENT_POSITION_X, y: CameraConstants.SCAN_COMPONENT_POSITION_Y }) this.ScanBorder() } .alignContent(Alignment.Center) .height(CameraConstants.SCAN_COMPONENT_WIDTH_100) .width(CameraConstants.SCAN_COMPONENT_WIDTH_100) .position({ x: CameraConstants.SCAN_COMPONENT_POSITION_X, y: CameraConstants.SCAN_COMPONENT_POSITION_Y }) .backgroundColor(Color.Grey) } .height(CameraConstants.SCAN_COMPONENT_WIDTH_100) .width(CameraConstants.SCAN_COMPONENT_WIDTH_100) .alignItems(HorizontalAlign.Center) .justifyContent(FlexAlign.Center) .backgroundColor(Color.White) } aboutToAppear(): void { Log.i(TAG, \'aboutToAppear\') this.initParams() // 注册XComp尺寸修改回调 this.customScanVM.regXCompSizeUpdateListener((width: number, height: number) => { // 动态更新XComponent的Surface尺寸 this.updateCameraSurfaceSize(width, height); }) // 注册扫描状态监听回调 this.customScanVM.regScanStatusUpdateListener((isPause: boolean) => { /*测试,resume ispause=false时,延时显示动画效果,检查是否黑屏*/ if (!isPause) { setTimeout(() => { this.pauseScan = isPause; }, 500); } else { this.pauseScan = isPause; } }) }}
1.2 插件编写
export class ScanPlugin implements FlutterPlugin, MethodCallHandler, AbilityAware { private channel: MethodChannel | null = null private flutterPluginBinding: FlutterPluginBinding | null = null private ability: UIAbility | null = null private mainWindow: window.Window | null = null; onAttachedToEngine(binding: FlutterPluginBinding): void { Log.i(TAG, \'onAttachedToEngine\') this.channel = new MethodChannel(binding.getBinaryMessenger(), CHANNEL_NAME) this.channel?.setMethodCallHandler(this) this.flutterPluginBinding = binding binding.getPlatformViewRegistry() .registerViewFactory(CHANNEL_VIEW_NAME, new ScanViewFactory(binding.getBinaryMessenger())) } onDetachedFromEngine(binding: FlutterPluginBinding): void { Log.i(TAG, \'onDetachedFromEngine\') this.channel?.setMethodCallHandler(null) this.channel = null } onDetachedFromAbility(): void { this.ability = null this.offWindowEvent() } onAttachedToAbility(binding: AbilityPluginBinding): void { this.ability = binding.getAbility() } onWindowEvent(context: Context) { try { if (this.mainWindow == null) { this.mainWindow = FlutterManager.getInstance() .getWindowStage(FlutterManager.getInstance().getUIAbility(context)) .getMainWindowSync(); this.mainWindow?.on(\'windowEvent\', (data: window.WindowEventType) => { if (data === window.WindowEventType.WINDOW_SHOWN) { AppStorage.setOrCreate(CameraConstants.CUSTOM_SCAN_PAGE_IS_BACKGROUND_NAME, false) } else if (data === window.WindowEventType.WINDOW_HIDDEN) { AppStorage.setOrCreate(CameraConstants.CUSTOM_SCAN_PAGE_IS_BACKGROUND_NAME, true) } }); } } catch (exception) { Log.e(TAG, \'Failed to register callback. Cause: \' + JSON.stringify(exception)); } } offWindowEvent() { try { this.mainWindow?.off(\'windowEvent\'); } catch (exception) { Log.e(TAG, \'Failed to register callback. Cause: \' + JSON.stringify(exception)); } } onMethodCall(call: MethodCall, result: MethodResult): void { this.onWindowEvent(this.ability!.context) try { switch (call.method) { case \"getPlatformVersion\": this.getInfo(result) break; case \"parse\": this.imgParse(call, result) break; default: result.notImplemented() break; } } catch (err) { Log.e(TAG, \'onMethodCall failed: \' + err); result.error(\"BarcodeScanPlugin\", \"onMethodCall failed with err\", err); } } getUniqueClassName(): string { Log.i(TAG, \'getUniqueClassName\') return TAG }}

其中最重要的就是下面这段,这段代码的作用是将一个自定义的原生视图工厂(ScanViewFactory)注册到 Flutter 引擎中。注册完成后,Flutter 端可以通过 CHANNEL_VIEW_NAME 来请求创建这个原生视图。具体来说,当 Flutter 端使用 AndroidView 或 UiKitView(在 iOS 中)时,Flutter 引擎会调用注册的 PlatformViewFactory 来创建对应的原生视图

binding.getPlatformViewRegistry() .registerViewFactory(CHANNEL_VIEW_NAME, new ScanViewFactory(binding.getBinaryMessenger()))

2. Flutter 端集成(增强健壮性)

2.1 插件接口定义
class Scan { static const MethodChannel _channel = const MethodChannel(\'chavesgu/scan\'); static Future<String> get platformVersion async { final String version = await _channel.invokeMethod(\'getPlatformVersion\'); return version; } static Future<String?> parse(String path) async { final String? result = await _channel.invokeMethod(\'parse\', path); return result; }} void _onPlatformViewCreated(int id) { _channel = MethodChannel(\'chavesgu/scan/method_$id\'); _channel?.setMethodCallHandler((MethodCall call) async { if (call.method == \'onCaptured\') { if (widget.onCapture != null) widget.onCapture!(call.arguments.toString()); } }); widget.controller?._channel = _channel; }
2.2 混合渲染页面
class _ScanViewState extends State<ScanView> { MethodChannel? _channel;  Widget build(BuildContext context) { if (defaultTargetPlatform == TargetPlatform.iOS) { return UiKitView( viewType: \'chavesgu/scan_view\', creationParamsCodec: StandardMessageCodec(), creationParams: { \"r\": widget.scanLineColor.red, \"g\": widget.scanLineColor.green, \"b\": widget.scanLineColor.blue, \"a\": widget.scanLineColor.opacity, \"scale\": widget.scanAreaScale, }, onPlatformViewCreated: (id) { _onPlatformViewCreated(id); }, ); } else if (defaultTargetPlatform == TargetPlatform.android) { return AndroidView( viewType: \'chavesgu/scan_view\', creationParamsCodec: StandardMessageCodec(), creationParams: { \"r\": widget.scanLineColor.red, \"g\": widget.scanLineColor.green, \"b\": widget.scanLineColor.blue, \"a\": widget.scanLineColor.opacity, \"scale\": widget.scanAreaScale, }, onPlatformViewCreated: (id) { _onPlatformViewCreated(id); }, ); } else if (defaultTargetPlatform == TargetPlatform.ohos) { return OhosView( viewType: \'chavesgu/scan_view\', creationParamsCodec: StandardMessageCodec(), creationParams: { \"r\": widget.scanLineColor.red, \"g\": widget.scanLineColor.green, \"b\": widget.scanLineColor.blue, \"a\": widget.scanLineColor.opacity, \"scale\": widget.scanAreaScale, }, onPlatformViewCreated: (id) { _onPlatformViewCreated(id); }, ); } else { return Text(\'平台暂不支持\'); } }}

以上代码根据不同的目标平台(iOS、Android 或 OpenHarmony)创建对应的原生视图。它通过 PlatformView(如 UiKitView、AndroidView 和 OhosView)来嵌入原生代码实现的功能(如二维码扫描)

四、关键技术点讲解

1. Flutter与HarmonyOSNEXT通信

在上述代码中,我们通过creationParams传递了部分数据给原生鸿蒙的view,那他如何接收呢,这就需要在鸿蒙端做处理了,我们在继承FlutterPlugin的类ScanPlugin中注册了一个工厂类ScanViewFactory,当有数据过来的时候,会通过binding.getBinaryMessenger()传递到这个类中

import { BinaryMessenger, PlatformView, Log, PlatformViewFactory, StandardMessageCodec} from \'@ohos/flutter_ohos\';import { ScanPlatformView } from \'../views/ScanPlatformView\';const TAG: string = \"FlutterScanPlugin\";export class ScanViewFactory extends PlatformViewFactory { private messenger: BinaryMessenger; constructor(messenger: BinaryMessenger) { super(StandardMessageCodec.INSTANCE); this.messenger = messenger; } public create(context: Context, id: number, args: Object): PlatformView { Log.i(TAG, \'create\') let params: Map<String, Object> = args as Map<String, Object> return new ScanPlatformView(context, this.messenger, id, params); }}

在create函数中获取到了一个HashMap,这个MAP里就是我们将要获取到的数据,在ScanPlatformView类的构造函数中我们将params拿出来,getView会将信息传递给最终要展示扫码界面的鸿蒙UI页面

@Observedexport class ScanPlatformView extends PlatformView implements MethodCallHandler, QRCodeReadListener {///省略部分代码... public getView(): WrappedBuilder<[Params]> { // 返回 WrappedBuilder,严格匹配基类要求 return new WrappedBuilder(CustomScanPage({ args: params })); }///省略部分代码...}@Componentexport struct CustomScanPage {///省略部分代码... aboutToAppear(): void { if(this.args!=null){ const scanPlatformView = this.args.platformView as ScanPlatformView; this.params = scanPlatformView.params!; } if (this.params) { this.mScale = this.params.get(\"scale\") as number this.customScanVM.setScale(this.mScale) Log.i(TAG, \'initParams mScale=\' + this.mScale) let r: number = this.params.get(\"r\") as number let g: number = this.params.get(\"g\") as number let b: number = this.params.get(\"b\") as number let a: number = this.params.get(\"a\") as number a = Math.max(0, Math.min(255, a)); r = Math.max(0, Math.min(255, r)); g = Math.max(0, Math.min(255, g)); b = Math.max(0, Math.min(255, b)); // const alpha = Math.max(0, Math.min(255, Math.floor(a * 256.0))); // 将 alpha 从 0-255 转换为 0-1 的浮点数 // const result = a / 255; // const alpha = Math.round(result * 100) / 100 Log.i(TAG, \'initParams scanLineColor a =\' + a) this.scanLineColor = this.rgbaToHex(a, r, g, b) }///省略部分代码...}

2. Flutter 端使用

class ScanPage extends StatelessWidget { ScanController controller = ScanController();  Widget build(BuildContext context) { return Scaffold( body: SafeArea( top: true, bottom: true, child: Stack( children: [ ScanView(  controller: controller,  scanAreaScale: .7,  scanLineColor: Colors.red,  onCapture: (data) { Navigator.push(context, MaterialPageRoute(  builder: (BuildContext context) {  return Scaffold(appBar: AppBar( title: Text(\'scan result\'),),body: Center( child: Text(data),),  );  }, )).then((value) {  controller.resume(); });  }, ), Positioned(  bottom: 0,  child: Row( children: [  ElevatedButton(  child: Text(\"toggleTorchMode\"),  onPressed: () {controller.toggleTorchMode();  },  ),  ElevatedButton(  child: Text(\"pause\"),  onPressed: () {controller.pause();  },  ),  ElevatedButton(  child: Text(\"resume\"),  onPressed: () {controller.resume();  },  ), ],  ), ), ], ), ), ); }}

通过本文的接收,相信开发者可快速实现 Flutter 与鸿蒙 Next 的深度集成,在保证跨平台 UI 一致性的同时,充分发挥鸿蒙原生能力。
声明:因原始仓库https://gitcode.com/openharmony-sig/fluttertpc_scan有BUG,无法提交PR,故我才重新上传了一份!