Flutter iPhone Duo checklist
Most Flutter apps adapt well to iPhone Duo, because Flutter’s layout system is resolution-independent. Whether yours does comes down to the patterns below, and each blocker is drawn further down the page.
- Works
- Works
- Breaks
- Breaks
- Breaks
- Works
- Breaks
- Works
- Breaks
- Looks dated
- Unverified
- Looks dated
- Unverified
- Needs native
Flutter already has the foldable abstraction
Flutter is in an unusual position. It already has the right abstraction for foldables, built for Android devices years ago, but there is no confirmation yet that it is wired up on iOS for iPhone Duo.
MediaQuery.displayFeatures reports hinges and cutouts, with bounds and state, and the TwoPane widget was built on top of it for Android foldables. It maps almost exactly onto Apple’s model: a hinge that splits the display is Apple’s division region, and a cutout that covers part of it is an occlusion region.
The layout system underneath is resolution-independent, so most screens need no fold-specific code at all. What changes is the range of widths they receive. The closed outer display is 466 points wide. The open inner display is 626 points wide held upright and 890 held sideways, and Split View can hand you anything in between.
MediaQuery.sizeOf(context).width, at 600 points, gives one column when closed and two when open, in either orientation. It also drops back to one column when Split View narrows the window, with no extra code. Apple’s advice for native apps is to target size classes rather than breakpoints; this framework has no size classes, so a single width check is our closest equivalent.Most Flutter apps already have a tablet breakpoint near 600 logical pixels. It will now fire on an iPhone, and the layout behind it is very likely the right one. Just make sure it is driven by width, not by platform.
The blockers, drawn on iPhone Duo
Each figure shows the same screen twice on the inner display, at its real proportions. On the left is the pattern most apps ship today, and on the right is the same screen after the fix. Below each drawing is the Dart that produces it.
Built against an older SDK
This one is not a crash. flutter build ios uses whichever Xcode is selected. An app built without the iOS 27 SDK runs at a familiar size and aspect ratio, with the rest of the display unused. The iOS 27.1 SDK is what lets it reach the screen edge.
xcrun --sdk iphoneos --show-sdk-version
# 26.x → a familiar size and ratioxcrun --sdk iphoneos --show-sdk-version
# 27.1 → extends to the screen edgeScreen size cached in State
A size read once and kept in a field goes stale the first time the device folds, rotates or enters Split View. Launch on the closed display, unfold, and everything built from that number stays sized for 466 points on a 626-point screen. Read MediaQuery.sizeOf in build. It rebuilds only when the size changes, which matters on a device where the size changes often.
double? _width;
@override
void didChangeDependencies() {
super.didChangeDependencies();
// Captured once, never updated
_width ??=
MediaQuery.sizeOf(context).width;
}@override
Widget build(BuildContext context) {
final w =
MediaQuery.sizeOf(context).width;
return SizedBox(
width: w - 32,
child: card,
);
}setPreferredOrientations ignored on the inner display
Apple states the inner display does not honour supported interface orientations, so a portrait lock silently stops applying. OrientationBuilder does not help either: it reports the shape of the screen, and at a ratio of 1.42 the shape tells you little. Branch on width instead.
SystemChrome.setPreferredOrientations([
DeviceOrientation.portraitUp,
]);final width =
MediaQuery.sizeOf(context).width;
return width >= 600
? const SidebarLayout()
: const StackLayout();Symmetric EdgeInsets
Flutter has always exposed left and right padding separately, so this is a matter of discipline rather than a missing API. Any EdgeInsets.symmetric built from one side, or any padding.horizontal / 2, assumes the two edges match. On iPhone Duo they do not.
final p = MediaQuery.paddingOf(context);
Padding(
padding: EdgeInsets.symmetric(
horizontal: p.left,
),
child: content,
)final p = MediaQuery.paddingOf(context);
Padding(
padding: EdgeInsets.only(
left: p.left,
right: p.right,
),
child: content,
)Split View looks frozen
Every app takes part in Split View on iPhone Duo. An app there can be visible while inactive, so video, animation controllers and timers paused on that state stop in front of the user.
@override
void didChangeAppLifecycleState(
AppLifecycleState state) {
if (state ==
AppLifecycleState.inactive) {
controller.pause();
}
}@override
void didChangeAppLifecycleState(
AppLifecycleState state) {
if (state ==
AppLifecycleState.paused) {
controller.pause();
}
}16:9 video on a 1.42 display
A centred 16:9 AspectRatio fitted to the 890-point width leaves 125 points of black bars. The natural Flutter answer is a Column: the player at 16:9, with related content in an Expanded beneath it.
Center(
child: AspectRatio(
aspectRatio: 16 / 9,
child: VideoPlayer(controller),
),
)Column(children: [
AspectRatio(
aspectRatio: 16 / 9,
child: VideoPlayer(controller),
),
const Expanded(child: RelatedList()),
])The open question: is displayFeatures populated on iOS?
Whether the iOS embedder fills displayFeatures from Apple’s reservedRegions API is not something Apple’s material settles, and Flutter has announced nothing. Assume the list is empty on iPhone Duo until a real build shows otherwise.
final hinges = MediaQuery.of(context)
.displayFeatures
.where((f) => f.type ==
DisplayFeatureType.hinge);
// [] → nothing to lay out aroundfinal mq = MediaQuery.of(context);
debugPrint('${mq.displayFeatures}');
// Populated → TwoPane patterns apply.
// Empty → a platform channel.If it is populated, the Android foldable patterns carry straight across, and Flutter is better placed than any other cross-platform stack. If it is empty, you can write a platform channel that wraps reservedRegions(kind:) and passes the frames into Dart, or accept a layout that is correct but unaware of the fold. ArrangementView and the vertical bar APIs have no Dart equivalent either way.
Find them in your codebase
Each of these searches maps to one of the blockers above.
grep -rn "MediaQuery.of(context).size" lib/
grep -rn "setPreferredOrientations" lib/
grep -rn "EdgeInsets.symmetric(horizontal" lib/
grep -rn "AppLifecycleState.inactive" lib/
Fix those four first. None of them depends on the answer to the displayFeatures question, and together they account for most of the visible improvement.
For the long-form version, including the breakpoint and lifecycle details, read Flutter on iPhone Duo: the right abstraction, possibly unwired.