duo_kit
Flutter layouts for iPhone Duo and other foldables.
On iPhone Duo, FoldScaffold moves your navigation into the strip at the side
of the window, and FoldSplitView puts a list and its details on either side
of the fold when the device opens like a book. duo_kit only places widgets: the
navigation and the panes are your own, in whatever design your app has.

Why duo_kit
Folding the device changes the window under a running app. duo_kit handles the parts of that which are easy to miss:
- State survives folding and unfolding. Close the device, open it, fold it
half open: the body of
FoldScaffoldstays the same widget, and the panes ofFoldSplitViewcarry their state between the split and your own layout. Scroll positions and text in fields come through every pose. A pane your layout hides can keep its state too, see Two panes. - No double insets. The navigation gets the padding of where it sits, the
camera and the status cluster included, so a plain
SafeAreakeeps it clear. The body and each pane drop the insets of the edges they do not touch, so nothing leaves a gap next to the navigation or the fold. - The keyboard stays in its half. Across a horizontal fold only the pane below it makes room for the keyboard; the pane above stays as it is.
- Dialogs open in the right half. With
FoldAnchoran alert opens above a horizontal fold and media controls below it. A dialog opened while the device lies flat moves to its half once the device folds. - Two panes anywhere in the tree.
FoldSplitViewfinds the fold from below a top bar or on a page of aPageView, and follows it as the page scrolls. - Look-alikes are ignored. A fold at the edge of a window in Split View splits nothing, the camera cutout of an Android phone in landscape is not taken for a strip, and an occlusion left over from the previous pose is dropped.
- Right-to-left languages. The first pane and the dialogs move to the other side.
- No iPhone Duo needed. Switch poses on any device without losing the app state, and test against window metrics measured on iPhone Duo.
Features
FoldGeometry– the strip, the fold and the occlusions of the window, computed fromMediaQueryalone.FoldScaffold– puts your navigation along the bottom edge or, where the window has a strip, in the strip, and pads the body to match.FoldSplitView– lays two panes out on either side of a fold, with your own layout while there is none, and keeps their state when the fold comes and goes.FoldAnchor– theanchorPointthat opens a dialog or a sheet in the right half of a window split by a fold.- Development tools – simulate a fold on any device and see the geometry over your app.
- Test poses – the window metrics of every iPhone Duo pose, for widget and golden tests.
duo_kit or adaptive_platform_ui
adaptive_platform_ui gives your app the platform's
own widgets: native UIKit toolbars and tab bars with Liquid Glass on iOS 26,
Cupertino on older iOS and Material on Android. On iPhone Duo it lays out its
own toolbar in the strip. Pick it if your app should look like the platform.
duo_kit draws nothing. It lays out the widgets you already have, in your own design, and handles the rest of the foldable: two panes split at the fold and dialogs that open in one half. It suits apps with a look of their own, or Material apps on iOS.
Installation
flutter pub add duo_kit
Where the geometry comes from
duo_kit reads only MediaQuery.viewPadding and MediaQuery.displayFeatures:
-
Android – Flutter fills the display features of a foldable by itself.
-
iOS – nothing fills them yet. Publish the fold and the camera with a plugin that reads the reserved regions of the window, such as
foldablewithDisplayFeatureBridgeMode.full:FoldableProvider( bridgeMode: DisplayFeatureBridgeMode.full, child: app, )Place it above your app, so that overlays outside the navigator see the fold too. While the device is closed,
foldablepublishes nothing; to keep the strip of the cover display clear of the status cluster, add its occlusions yourself, asClosedFoldOcclusionsin the example does.duo_kit_ios, in development in the same repository, will publish all of them natively, the closed device included.With the fold published, Flutter keeps dialogs, sheets and pickers on one side of it while the device is half open, as it does on Android.
iPhone Duo setup
- Build with the iOS 27.1 SDK (Xcode 27.1). With an older SDK, iOS runs the app in compatibility mode, with no strip and no reserved regions.
- Adopt the UIScene life cycle. With the iOS 27 SDK an app without it
crashes at launch. Projects created with Flutter 3.41 already have it; in an
older one, make your
AppDelegateaFlutterImplicitEngineDelegate, register plugins indidInitializeImplicitFlutterEngine, and add aUIApplicationSceneManifestwithFlutterSceneDelegatetoInfo.plist. - Target iOS 15.0 or later, in the Runner target and in your pods. Xcode 27 rejects lower deployment targets, and the Flutter 3.41 template still sets 13.0.
- Do not lock the orientation. With
UIRequiresFullScreenand portrait as the only orientation inInfo.plist, iOS scales the app into a portrait window and reports no reserved regions, so no fold. On the simulator the missing regions stay with the bundle ID after the lock is gone, until the simulator reboots. A lock set withSystemChrome.setPreferredOrientationskeeps the fold but turns the UI on its side on the open device.
Usage
Navigation
FoldScaffold(
body: SafeArea(bottom: false, child: page),
navigationBuilder: (context, placement) => MyNavigationBar(
// Lay the items out along placement.axis: a row at the bottom, a column
// in the strip.
axis: placement.axis,
),
)
The navigation gets the MediaQuery padding of where it sits – the bottom
inset, or in the strip the room taken by the camera and the status cluster – so
a SafeArea inside it keeps it clear. The body stays the same widget in every
pose and keeps its state.

In a strip, placement is left or right, the side the strip runs along. On
iPhone Duo it is left only on the cover display in landscape, with the camera
on the left. Put whatever faces the content, such as a divider or a selection
indicator, on the edge towards the body.
Two panes
FoldSplitView(
first: list,
second: details,
unfoldedBuilder: (context, first, second) => Row(
children: [
SizedBox(width: 320, child: first),
Expanded(child: second),
],
),
)
While a fold crosses it, FoldSplitView puts first on the leading side of a
vertical fold or above a horizontal one, and second on the other side. Each
pane keeps only the padding and the view insets, such as the keyboard, of the
edges it touches. Without a fold it uses your layout; put each pane in it once.
Decide when two panes fit with your own breakpoint.
A pane your layout leaves out loses its state. To keep it, hide it instead:
unfoldedBuilder: (context, first, second) => Stack(
fit: StackFit.expand,
children: [
first,
Offstage(child: TickerMode(enabled: false, child: second)),
],
),
Dialogs and sheets
showDialog(
context: context,
anchorPoint: FoldAnchor.content.resolvePoint(context),
builder: (context) => const MyDialog(),
);
FoldAnchor.content opens above a horizontal fold, FoldAnchor.controls below
it; across a vertical fold both open on the trailing side. Overlays that are not
routes can do the same with DisplayFeatureSubScreen.
Geometry
final geometry = FoldGeometry.of(context);
geometry.strip; // The strip at the side, with the clearances of its occlusions.
geometry.division; // The fold that splits the window, if any.
geometry.occlusions; // Cameras and the status cluster.
geometry.availableWidth; // The width without the side insets.

The cover display and the inner display lying flat, then half open like a book
and like a laptop, as FoldGeometryOverlay paints them: the strip in blue, the
camera and the status cluster in orange, and the fold in red.
Development tools
import 'package:duo_kit/debug.dart';
kDebugMode ? FoldDebugTools(child: app) : app
A button at the left edge switches between poses – a strip, a fold like a book, a fold like a laptop – on any device, and holding it shows or hides the geometry over your app. Place it below whatever publishes the real fold.
Tests
import 'package:duo_kit/testing.dart';
MediaQuery(data: FoldTestPose.duoHalfOpenedLandscape.data, child: screen)
Limitations
- A strip is looked for on iOS only, since an Android phone in landscape can
report the same insets for its camera cutout. Pass
detectStripto change that. - Apple's guidelines for iPhone Duo put the strip on the left for the app on the left of Split View, but iOS 27.1 reports no strip to a Flutter app there, so its navigation stays at the bottom.
FoldSplitViewmeasures where it sits after it builds, resizes or scrolls, and catches up one frame later.- With
foldable, the fold reaches the display features about a second after the device comes to rest half open, since the reserved regions follow the hinge with a delay. - Tested on the iPhone Duo simulator. Android foldables follow from the same display features, but have not been tested on a device.
🛠️ Maintained by LeanCode
This package is built with 💙 by LeanCode. We are top-tier experts focused on Flutter Enterprise solutions.
Why LeanCode?
- Creators of Patrol – the next-gen testing framework for Flutter.
- Full-Cycle Product Development – We take your product from scratch to long-term maintenance.

