关于 iphone-duo-flutter
面向 Flutter 开发者的 iPhone Duo 适配指南,介绍用约束和独立安全区构建自适应布局,并说明如何通过平台通道接入折叠区域等原生信息。
主要功能
- 01用约束处理窗口尺寸变化
- 02分别处理各边安全区内边距
- 03按宽度断点切换导航布局
- 04通过平台通道查询区域并接收铰链事件
适用场景
- 01修复开合或分屏时的布局问题
- 02为内外屏切换导航栏与导航轨
- 03让交互控件避开折叠与摄像头区域
安装
复制下面的命令,在终端中运行。
npx skills add https:/ / github.com/ mirzaaghazadeh/ iphone-duo-skills --skill iphone-duo-flutter安装后在 Claude Code 中输入/iphone-duo-flutter确认已加载。
详细介绍
iPhone Duo 上的 Flutter
请从这里开始,否则这件事会浪费你一下午的时间:
MediaQuery.displayFeatures的文档说明它“仅在 Android 上填充”。
Flutter 的折叠屏抽象是为 Android 铰链设计的。在 iPhone Duo 上,无论设备折叠到什么程度,它都会返回空列表。dual_screen 软件包(TwoPane、hingeAngleEvents)也一样——实际上仅适用于 Android,且上次发布已是大约三年前。
因此,Flutter 开箱后无法感知这台设备的折叠状态。不过,它确实提供了大部分真正重要的功能。分两个层级处理。
第 1 层——现在就能用,无需原生代码
优秀移植工作的主体都在这一层,而且完全使用 Dart 实现。
正确处理尺寸变化
iPhone Duo 会不断改变应用的尺寸:打开、合上、折叠、旋转以及进入 Split View。Flutter 会处理这些变化,但前提是布局由约束驱动,而不是由固定值驱动。
- 使用
LayoutBuilder及其BoxConstraints做布局决策。 - 需要窗口尺寸时,使用
MediaQuery.sizeOf(context),而不是MediaQuery.of(context)——sizeOf/paddingOf访问器只会在所读取的属性变化时触发重建,这一点在尺寸如此频繁变化时很重要。 - 绝不要在
initState或字段中缓存尺寸。几秒内它就会过时。
处理不对称安全区域
这是会导致实际故障的问题。在 iPhone Duo 上,系统控件位于一侧,因此内边距通常是不对称的——具体位于哪一侧,取决于设备姿态以及你占据 Split View 的哪一半。
MediaQuery.paddingOf(context) 会返回 EdgeInsets,其中 left、
top、right、bottom 各自独立。请分别使用。任何类似
width - padding.left * 2 的表达式,在这台设备上都是缺陷。
SafeArea 微件已经能独立处理四条边,因此应优先使用它,而不是手动编写内边距。如果需要让插画延伸到安全区域下方,请将背景放在 SafeArea 外,只将可交互的前景放在其中。
用尺寸类别思路取代断点
iOS 使用尺寸类别来处理这台设备:外屏是紧凑宽度,内屏是常规宽度。Flutter 没有尺寸类别,因此可以映射到宽度断点——Material 3 窗口尺寸类别是合理的参考(紧凑 < 600、中等 600–840、扩展 ≥ 840 逻辑像素)。
两条规则:
- 根据传入的约束进行分支,而不是根据
Platform.isIOS、设备型号字符串或MediaQuery.orientationOf。内屏不遵循受支持的方向设置,因此方向不是可靠的信号。 - 根据你的内容何时无法正常显示来选择断点,然后检查紧凑/常规的划分是否恰好适用。
优先使用自适应容器
宽布局使用 NavigationRail,窄布局使用 NavigationBar,这相当于 Flutter 实现了 iOS 原生将控件移到侧边的行为。内屏使用侧边栏、外屏使用导航栏,效果会比较贴合,而且只需一个断点。
第 2 层——需要平台通道
所有真正针对 iPhone Duo 的功能,在桥接之前 Dart 都无法使用。截至本文撰写时,还没有 Flutter 版本发布对 iPhone Duo 的支持,因此请做好自行编写的准备,并在动手前确认该支持是否已加入。
值得桥接的功能,按优先级排序如下:
| 原生 API(iOS 27.1) | 在 Dart 中使用的原因 |
|---|---|
UIView 上的 reservedRegions(kind:) | 避免控件落在折叠处或屏下摄像头区域 |
UIHingeInteraction | 获取连续的铰链角度,用于实现效果 |
traitCollection 中的尺寸类别 | 获取 iOS 使用的真实信号,而不是依赖你猜测的断点 |
如何设计桥接
对于一次性查询(当前保留区域、当前尺寸类别),使用 MethodChannel;对于任何持续变化的数据,使用 EventChannel——铰链角度是实时数据流,用 MethodChannel 会产生大量调用。
在 iOS 端的 Runner 目标中,在 FlutterViewController 上注册 FlutterMethodChannel 和 FlutterEventChannel,然后从该视图读取原生 API。保留区域 API 属于 UIView,而 FlutterViewController 的视图正是要查询的正确视图——Dart 布局实际占据的就是这个视图。
将区域序列化为普通映射({left, top, width, height, kind, active}),这样 Dart 就能据此重建 Rect。区域发生变化时发送区域列表,而不是轮询。
所有操作都要做好防护:在所有非折叠设备上,铰链值都是 nil;而且这些 API 在 iOS 27.1 以下不存在。桥接必须在遇到这些情况时退化为“无区域、无铰链”,而不是抛出异常,因为同一个 Flutter 二进制文件会运行在每一台 iPhone 上。
然后像 Tier 1 那样使用
区域到达 Dart 后,将其传入你已经构建好的同一套约束驱动布局。分隔区域(折叠处)会告诉你哪些位置不要放置交互式组件;遮挡区域(摄像头)会告诉你要避免遮挡哪些位置。通常,使用 Stack 搭配 Positioned,或自定义 MultiChildLayoutDelegate,是让特定控件避开区域的最简洁方式。
按以下顺序操作
- 让应用在
LayoutBuilder下能够干净地调整大小。大多数 Bug 都能在这里解决。 - 检查每一处使用
MediaQuery.padding的代码,留意是否进行了对称内边距计算。 - 用基于约束的分支逻辑替代方向和平台检查。
- 增加分栏布局,让内屏不只是拉伸后的手机布局。
- 只有在应用确实需要折叠感知时,才编写通道。
步骤 1–4 能让 Flutter 应用在 iPhone Duo 上的大部分体验都达到理想状态,同时也能改善它在平板电脑和桌面设备上的表现。步骤 5 是唯一专为 iPhone Duo 设计的部分,也是唯一无法在没有 Xcode 27.1 模拟器的情况下测试的部分。
要了解原生 API 的实际功能及其原因,请阅读
../iphone-duo-adaptive-layout/SKILL.md 和
../iphone-duo-readiness/SKILL.md——尽管代码不能直接迁移,设计思路可以直接借鉴。