duo_kit 0.1.1
duo_kit: ^0.1.1 copied to clipboard
iPhone Duo and foldable layouts for Flutter: navigation in the strip, a split view across the fold and dialogs in one half, around your own widgets.
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.

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 on a device with a hinge. iOS then scales the app instead of laying it out, and reports no fold. On the simulator this sticks to the bundle ID until the simulator reboots.
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.


