Flutter iOS Host App:Add to App 集成场景的宿主应用与 UI 测试实践 Flutter iOS Host AppAdd to App 集成场景的宿主应用与 UI 测试实践【免费下载链接】flutterFlutter makes it easy and fast to build beautiful apps for mobile and beyond项目地址: https://gitcode.com/GitHub_Trending/flutter41/flutter本文基于 Flutter 仓库中的 dev/integration_tests/ios_host_app 集成测试工程系统讲解这个iOS 宿主应用host app的设计意图与完整实现它如何作为flutter create -t module生成模块的宿主容器演示 Add to App 的五类核心场景原生基线、全屏冷/热引擎、混合视图、双引擎并行、动态内容缩放并通过 XCUITest 自动化验证引擎预热复用、平台通道通信与多引擎并发等关键能力。读完后你将掌握在原生 iOS 应用中嵌入 Flutter 的完整接入方式以及官方 CIdevicelabmodule_test_ios对该场景的验证手段。工程定位为 devicelab 模块测试服务的宿主应用该工程的 README.md 开宗明义Used by themodule_test_ios.dartdevice lab test.即这个宿主应用首先是 Flutter 官方 devicelab 持续集成任务module_test_ios的被测对象。其结构分三层Host/—— 原生 iOS 宿主应用Objective-C包含全部演示用的 ViewControllerflutterapp/—— 通过flutter create -t module hello创建的 Flutter 模块的lib目录承载 Dart 侧演示代码FlutterUITests/—— 基于 XCUITest 的 UI 自动化测试FlutterUITests.m。工程还带有 Podfile 与 PodfileMissingPostInstall从源码结构看后者用于在 CI 中模拟Podfile 缺少 Flutter 模板 post_install 钩子的异常路径配合module_test_ios任务做故障注入式验证。准备工作创建 Flutter 模块并接入宿主应用README 给出的接入步骤如下用命令创建一个 Flutter 模块$ flutter create -t module hello将模块放到宿主应用的同级目录sibling folder中把本仓库flutterapp/lib/下的marquee.dart与main.dart复制到新模块的lib目录中——这是为了让平台单元测试通过因为FlutterUITests断言的页面元素如Button tapped 0 times.、This is Marquee来自这些 Dart 文件。当前仓库中的 Dart 侧源码位于flutterapp/lib/main —— 应用入口与路由flutterapp/lib/marquee —— 跑马灯演示页marquee_green/marquee_purple两个路由供双引擎演示使用flutterapp/lib/resize —— 动态缩放演示页resize路由。宿主应用首页六个演示入口首页 MainViewController.m 以UIStackView垂直排列六个按钮分别对应 README 中列出的全部演示场景按钮目标页面引擎状态Native iOS ViewNativeViewController纯原生基线Full Screen (Cold)FullScreenViewController新建引擎冷启动Full Screen (Warm)FullScreenViewController复用预置引擎热引擎Flutter View (Warm)裸FlutterViewController复用预置引擎热引擎Hybrid View (Warm)HybridViewController热引擎Dual Flutter View (Cold)DualFlutterViewController两个独立冷引擎Dynamic Content ResizingDynamicResizingViewController冷启动首页同时暴露了两个便捷访问器供各子页面直接取用预置引擎和 reload 通道- (FlutterEngine *)engine { return [(AppDelegate *)[[UIApplication sharedApplication] delegate] engine]; } - (FlutterBasicMessageChannel*)reloadMessageChannel { return [(AppDelegate *)[[UIApplication sharedApplication] delegate] reloadMessageChannel]; }引擎预热AppDelegate 中冷/热的本质冷引擎cold与热引擎warm的区别集中在 AppDelegate.m。应用启动时立即创建并运行一个命名引擎static NSString *_kReloadChannelName reload; - (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions { _engine [[FlutterEngine alloc] initWithName:test project:nil]; [_engine runWithEntrypoint:nil]; _reloadMessageChannel [[FlutterBasicMessageChannel alloc] initWithName:_kReloadChannelName binaryMessenger:_engine.binaryMessenger codec:[FlutterStringCodec sharedInstance]]; return [super application:application didFinishLaunchingWithOptions:launchOptions]; }两个关键设计FlutterEngine与FlutterViewController解耦引擎在 App 启动时即被创建并runWithEntrypoint:与任何 UI 无关。后续任何页面都可以通过initWithEngine:把同一个引擎挂到新的FlutterViewController上这正是pre-warm 引擎并反复 attach/detach ViewController能力的实现基础。reload通道在引擎的binaryMessenger上建立了一个名为reload的FlutterBasicMessageChannel字符串编解码用于向 Dart 侧推送目标路由。每次进入新页面前先发消息如full、/再创建 ViewController保证 Dart 侧 UI 提前就绪。对应的冷启动入口showFullScreenCold则不经过预置引擎- (void)showFullScreenCold { FullScreenViewController *flutterViewController [[FullScreenViewController alloc] init]; // 内部创建全新引擎 [flutterViewController setInitialRoute:full]; [[self reloadMessageChannel] sendMessage:full]; [self.navigationController pushViewController:flutterViewController animated:NO]; }而热引擎入口showFullScreenWarm先在引擎的导航通道上设置初始路由再复用引擎构造页面- (void)showFullScreenWarm { [[self engine].navigationChannel invokeMethod:setInitialRoute arguments:full]; [[self reloadMessageChannel] sendMessage:full]; FullScreenViewController *flutterViewController [[FullScreenViewController alloc] initWithEngine:[self engine] nibName:nil bundle:nil]; [self.navigationController pushViewController:flutterViewController animated:NO]; }值得注意的是两处animated:NO源码注释说明冷引擎场景下带转场动画会出现明显卡顿Its especially bad with a cold engine。场景一全屏 Flutter 页面与引擎 detachFullScreenViewControllerFullScreenViewController.m 是FlutterViewController的子类覆盖全屏并隐藏导航栏与状态栏。其核心是引擎的 detach 时机-(void)viewWillDisappear:(BOOL)animated { [super viewWillDisappear:animated]; self.navigationController.navigationBarHidden NO; self.navigationController.hidesBarsOnSwipe NO; if (self.isMovingFromParentViewController) { // If we needed Flutter to tell us when we could actually go away, // wed need to communicate over a method channel with it. [self.engine setViewController:nil]; } }setViewController:nil在确认正在被弹出返回导航栈isMovingFromParentViewController时把引擎从当前 ViewController 上解绑。源码注释特别提示如果页面使用了image_picker等可能唤起系统 UI 的插件此处不能盲目 detach如需判断何时可以真正离开应通过 method channel 与 Dart 侧协商。这一细节体现了引擎生命周期管理在真实业务中的边界条件。场景二混合视图与平台通道双向交互HybridViewControllerHybridViewController.m 演示同一个屏幕上同时存在原生视图与 Flutter 视图并用平台通道让二者互相通信。布局上用一个等分垂直UIStackView上下各放一半上半部分是NativeViewController作为 childViewController 嵌入delegate 指向自身下半部分是基于预置热引擎创建的FlutterViewController。通道部分注意使用的是FlutterBasicMessageChannel 字符串编解码channel 名为incrementstatic NSString *_kChannel increment; static NSString *_kPing ping; // 从 Flutter 侧的消息通道Dart 点击后向原生发 ping _messageChannel [[FlutterBasicMessageChannel alloc] initWithName:_kChannel binaryMessenger:_flutterViewController.binaryMessenger codec:[FlutterStringCodec sharedInstance]]; __weak NativeViewController *weakNativeViewController nativeViewController; [_messageChannel setMessageHandler:^(id message, FlutterReply reply) { [weakNativeViewController didReceiveIncrement]; // 原生计数 1 reply(); }]; // 原生 - Flutter 方向 - (void)didTapIncrementButton { [_messageChannel sendMessage:_kPing]; }交互闭环是Dart 侧 Flutter 按钮点击 → 经increment通道发ping→ 原生NativeViewController计数 1 并刷新文案原生 Increment via iOS 按钮点击 → 原生发ping→ Dart 侧计数 1。注意__weak弱引用避免了 VC 之间的循环引用这是宿主应用中嵌入 Flutter 时容易踩的坑。嵌入子视图的标准三部曲也在此体现addChildViewController:→addArrangedSubview:→didMoveToParentViewController:。场景三双引擎并行DualFlutterViewControllerDualFlutterViewController.m 验证同一 App 内同时运行两个 Flutter 引擎_topFlutterViewController [[FlutterViewController alloc] init]; _bottomFlutterViewController [[FlutterViewController alloc] init]; [_topFlutterViewController setInitialRoute:marquee_green]; [_bottomFlutterViewController setInitialRoute:marquee_purple];两次不带引擎参数的init会各自创建独立引擎再通过setInitialRoute:分别路由到marquee_green与marquee_purple两条跑马灯页面。UI 测试 FlutterUITests.m 中testDualCold用谓词断言页面上恰好存在两个This is Marquee文本元素count 2作为两个引擎独立渲染的验收标准。场景四动态内容缩放DynamicResizingViewControllerDynamicResizingViewController.m 演示 Flutter 视图作为长列表中的一项参与原生自动布局在一个UIScrollViewUIStackView里排布 50 个原生UILabel第 10 项替换为FlutterViewController_flutterViewController [[FlutterViewController alloc] init]; [_flutterViewController setInitialRoute:resize]; _flutterViewController.autoResizable YES; // 允许随布局动态改变尺寸autoResizable YES是关键它让 Flutter 视图可以按容器约束改变大小。UI 测试testResize会先断言flutter_view的初始高度为 100点击后断言高度变为 200通过accessibilityIdentifier flutter_view定位验证 Flutter 视图尺寸变化时与原生滚动布局的正确联动。自动化验收FlutterUITests 验证的三个关键点README 明确列出该工程测试的三件核心事情能预热引擎并反复从引擎 attach/detach ViewController能用平台通道在视图之间通信能同时运行两个引擎实例。FlutterUITests.m 以 XCUITest 逐一覆盖超时统一 60 秒testFullScreenColdPop/testFullScreenWarm进入全屏页面后断言Button tapped 0 times.出现点击 Flutter 侧Increment via Flutter元素后断言计数变为 1再点POP回到Flutter iOS Demos Home导航栏。冷、热两条路径走的是同一套断言从而对比验证新建引擎与复用预热引擎两种 attach 方式的 UI 正确性testFlutterViewWarm验证裸FlutterViewControllerchild view 式 push在热引擎下的交互testHybridViewWarm进入混合页面后同时断言 Flutter 侧与 Platform 侧两个计数器初始均为 0分别点击两个方向的按钮并验证对应计数器递增、另一个不变完整覆盖双向通道通信testDualCold用NSPredicate(count 2)断言两个跑马灯元素同时存在testResize断言 Flutter 视图高度 100 → 200 的动态缩放。测试中还体现了真实的健壮性处理例如testFullScreenWarm对首次点击未生效时会重试一次XCUITest tap 竞态testFlutterViewWarm针对 iOS 17 模拟器偶发重启的二次重试与os_log诊断输出。统一封装的waitForAndTapElement:先用exists YES AND hittable YES谓词等待元素可点击再 tap避免了对未就绪元素的盲点击。小结与延伸阅读该宿主应用是理解 Flutter Add to App 原生侧接入方式的精炼样本其覆盖的 API 面包括FlutterEngine预创建、initWithEngine:复用、setViewController:nil解绑setInitialRoute:与navigationChannel的setInitialRoute方法通道控制初始路由FlutterBasicMessageChannel双向字符串消息通道多引擎并行与autoResizable动态布局。如需进一步了解该工程在 CI 中的执行方式可参考 devicelab 任务框架dev/devicelab同目录下还有 ios_add2app_life_cycle 等 Add to App 相关集成测试工程可与本文的工程对照阅读。【免费下载链接】flutterFlutter makes it easy and fast to build beautiful apps for mobile and beyond项目地址: https://gitcode.com/GitHub_Trending/flutter41/flutter创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

相关新闻

最新新闻

在 Transformers 中使用 Chinese-CLIP:中文图文对比预训练模型的架构解析与跨模态检索实战指南

在 Transformers 中使用 Chinese-CLIP:中文图文对比预训练模型的架构解析与跨模态检索实战指南

在 Transformers 中使用 Chinese-CLIP:中文图文对比预训练模型的架构解析与跨模态检索实战指南 【免费下载链接】transformers 🤗 Transformers: the model-definition framework for state-of-the-art machine learning models in text, vision, audio,…

2026/9/7 9:18:15
Dify 工作区成员邀请弹窗:从 README 到源码的收件人状态机与表单校验全解析

Dify 工作区成员邀请弹窗:从 README 到源码的收件人状态机与表单校验全解析

Dify 工作区成员邀请弹窗:从 README 到源码的收件人状态机与表单校验全解析 【免费下载链接】dify Build Agentic workflows, RAG pipelines, with rich AI model and tool support on one collaborative workspace. Deploy on cloud, VPC, or self-hosted, so team…

2026/9/7 9:18:15
FanControl 教程:让机箱风扇安静下来的 3 个配置

FanControl 教程:让机箱风扇安静下来的 3 个配置

FanControl 教程:让机箱风扇安静下来的 3 个配置 【免费下载链接】FanControl.Releases This is the release repository for Fan Control, a highly customizable fan controlling software for Windows. 项目地址: https://gitcode.com/GitHub_Trending/fa/FanC…

2026/9/7 9:18:15
C# Winform酒店管理系统开发全流程详解:从设计到部署实战

C# Winform酒店管理系统开发全流程详解:从设计到部署实战

简介:一套基于 C# Winform 与 SQL Server 的管理系统项目,工程内部命名为天秀酒店管理系统,整体定位为 C# 桌面应用方向的大作业或毕业设计参考。系统包含管理员与学生两类用户角色,管理员可维护管理员信息、管理学生信息&#xf…

2026/9/7 9:18:15
Redis可视化客户端选型与实战排查指南

Redis可视化客户端选型与实战排查指南

简介:面向Windows用户提供的Redis可视化工具资源包,封装了Redis Desktop Manager及运行所需组件,使开发者可以脱离命令行完成Redis服务器连接、键值浏览、增删改查及命令执行等日常操作,特别适合需要快速上手Redis的初学者和希望提…

2026/9/7 9:18:15
统信UOS误删文件急救指南:从回收站到ext4深度恢复

统信UOS误删文件急救指南:从回收站到ext4深度恢复

误删文件后,第一件要做的事不是到处查教程,而是先停手。统信UOS(UOS)基于Debian生态,桌面环境默认文件系统通常是ext4,误删一个文件之后,能不能恢复、能恢复到什么程度,往往不取决于…

2026/9/7 9:13:15