unfold

Adapt your Flutter app to iPhone Duo the way Apple asks, from UIKit's own APIs, and to Android foldables with the same code.

Flutter does not report any of iPhone Duo's geometry: MediaQuery.displayFeatures is filled on Android only. This plugin reads it natively and streams it to Dart.

The example app on the iPhone Duo outer display: the tabs and two actions sit in the side column under the status bar
UIKit (iOS 27.1 SDK) What you get
UIView.reservedRegions(kind: .occlusion) Areas covered by the cameras and the status bar column, inactive ones included
UIView.reservedRegions(kind: .division) Where content divides in two: the fold
UIView.LayoutRegion.bar(onEdge:extent:) Where UIKit places a bar along each side edge
UITraitCollection.verticalBarEdge The side the system puts the vertical bar on
UIHingeInteraction / UIHinge Hinge posture (closed, partially open, fully open) and angle
horizontalSizeClass, verticalSizeClass The size classes iOS lays out from
preferredVerticalBarBehavior Turning the vertical bar off, for games and video

On Android (Pixel Fold, Galaxy Fold, Surface Duo) the fold and posture come from Flutter's MediaQuery.displayFeatures, with no native code. Both platforms arrive as one DeviceGeometry, updated live as the device folds, unfolds or rotates.

Use

MaterialApp(
  builder: (context, child) => DeviceGeometryScope(child: child!),
  home: const Home(),
);
AdaptiveNavigationScaffold(
  title: const Text('Inbox'),
  selectedIndex: tab,
  onDestinationSelected: (i) => setState(() => tab = i),
  destinations: const [
    AdaptiveDestination(icon: Icons.inbox_outlined, selectedIcon: Icons.inbox, label: 'Inbox', badge: 3),
    AdaptiveDestination(icon: Icons.settings_outlined, label: 'Settings'),
  ],
  actions: [
    AdaptiveAction(icon: Icons.edit, label: 'Compose', priority: 1, onPressed: compose),
    AdaptiveAction(icon: Icons.archive_outlined, label: 'Archive', onPressed: archive),
  ],
  body: pages[tab],
);
  • iPhone Duo, vertical bar: actions go at the top of the status bar's column and the tabs at the bottom, as UIKit places a toolbar and a tab bar there. When they do not fit, compression decides what gives way: prefersTabBar (the default) moves the lowest-priority actions to an overflow menu; prefersActions drops the tab labels first.
  • Wide windows (840 and up): a NavigationRail, actions in the app bar.
  • Phones: a NavigationBar, actions in the app bar.

Give every action a label and an icon: Apple asks for both on any item that can move to the vertical bar.

Read only what you need

Each reader rebuilds only when its part changes. The hinge angle is not part of the geometry at all, so nothing relayouts while the hinge moves.

DeviceGeometryScope.foldOf(context);               // Fold? in effect now
DeviceGeometryScope.hingeOf(context);              // HingeStatus?
DeviceGeometryScope.horizontalSizeClassOf(context); // SizeClass
DeviceGeometryScope.sideStripOf(context);          // SideStrip?
context.deviceGeometry;                            // everything
final g = context.deviceGeometry;
g.fold;              // Fold? — bounds, vertical, isBook, isTabletop, obstructs
g.divisionLine;      // Rect? — where the window can divide, folded or not
g.evenColumns(3);    // 4 when the window can divide, so no column straddles the fold
g.barEdge;           // StripSide? — the system's side for the vertical bar
g.occlusions;        // [ReservedRegion] — frame, margins, active
g.sideBars;          // {StripSide: Rect} — UIKit's bar regions
g.supported;         // iOS: are UIKit's regions available?

A fold is in effect only while the device is partially open. On iPhone Duo a division's active flag follows the hinge with a delay, so fold checks the posture too.

Widgets

  • FoldSplit(primary:, secondary:) puts two panes side by side, divided on the fold line 50/50 as Apple's split views are. The line stays put while the device opens flat, so nothing jumps; the fold's width is left empty only while it is in effect.
  • TabletopSplit(top:, bottom:) puts content above the fold and controls below it while the device stands half-open.
  • OcclusionSafeArea(child:) moves its child off the camera and status bar regions it overlaps. Apple warns that touches there may not register, and safe areas alone do not cover them.
  • HingeAngleBuilder(builder:) rebuilds with each hinge angle and nothing above it. Use the angle for effects; Apple asks that layout follow the posture and size classes.

Outside the tree, Unfold.hingeAngle is a stream of degrees: 0 folded shut, 180 flat.

Dialogs and sheets on one side of the fold

Flutter keeps dialogs, bottom sheets, menus and pickers on one side of a display feature that splits the window. Opt in to give it iPhone Duo's:

DeviceGeometryScope(bridge: DisplayFeatureBridge.full, child: child!)
bridge Effect
none (default) Nothing published. Framework behavior does not change.
cutouts Active camera regions, as cutouts. Surfaces keep their full width.
full The fold too, while the device is partially open. Surfaces open on one side; give them an anchorPoint to choose which.

Where Flutter reports display features itself, the bridge stands down.

Turning the vertical bar off

For a full-screen video player, a game or a non-scrolling layout, Apple lets an app keep horizontal bars. Set the Flutter view controller's class in ios/Runner/Base.lproj/Main.storyboard to UnfoldFlutterViewController (module unfold), then:

await Unfold.setVerticalBarEnabled(false);

The status bar goes back to the horizontal axis and the side column is no longer reserved. Apple asks that this stay a stable choice, not something that follows the screen's state.

iPhone Duo: what Apple asks

  • Build with Xcode 27.1 (iOS 27.1 SDK). With older SDKs iOS shows the app at a familiar iPhone size, and supported is false.
  • Lay out from the window's size and size classes, not from the device, its orientation or UIScreen.main. Regular width does not mean iPad.
  • Design for the two size classes, not for each pose. The inner display should show one more level of hierarchy, such as a list and its detail, rather than a stretched phone layout.
  • Keep interactive content out of the fold and out of a region's margins. Scrolling content does not need to avoid the fold.
  • Use an even number of grid columns when the window can divide.
  • From April 2027, App Store submissions need iPhone Duo screenshots: 1398 × 2034 for the outer display and 2007 × 2853 for the inner one.

If occlusions stays empty on iPhone Duo although supported is true, the system may be running that bundle identifier in compatibility mode, because a build made with an older SDK ran on the device first. Erase the simulator (Device → Erase All Content and Settings) or try another bundle identifier.

Android foldables

  • Put the split of a list and its detail on the fold, and leave a hinge's width empty (fold.obstructs). FoldSplit does both.
  • Use tabletop posture (fold.isTabletop) for content on top and controls below.
  • Declare android:resizeableActivity="true" and handle screenSize|smallestScreenSize|screenLayout|orientation in configChanges, so folding does not restart the activity.

Swift Package Manager and CocoaPods

The plugin ships a Package.swift, which Flutter uses by default since 3.44, and a podspec for apps that still build with CocoaPods. Flutter asks plugins to support both until further notice. Both include the privacy manifest.

Migrating from duo_layout

unfold is the new name of duo_layout. Replace the dependency and the import, then:

  • geometry.hinge is a HingeStatus?; the angle moved to Unfold.hingeAngle and HingeAngleBuilder.
  • ReservedRegion.active now matters: inactive regions are included.
  • fold is null while the device lies flat, even if the division still reads active.

Platforms

Flutter 3.44+ (Dart 3.12+), iOS 15+ and Android. UIKit's regions, bars and hinge need the iOS 27.1 SDK at build time and iOS 27.1 at run time; otherwise they are empty, and the package still builds with older Xcode versions. On other platforms the geometry is empty, and AdaptiveNavigationScaffold falls back to a rail or a bottom bar.

Example

example/ is a three-tab app on AdaptiveNavigationScaffold, with two actions: one opens a dialog, the other turns the vertical bar off.

  • Geometry: everything the device reports, including the live hinge angle.
  • Split: a list and a grid divided on the fold, with an even column count.
  • Tabletop: content above the fold and controls below.

Run it on the iPhone Duo simulator (Xcode 27.1) and on an Android foldable emulator.

Libraries

unfold
Layouts for iPhone Duo from UIKit's own APIs, plus Android foldables.