duo_dynamic_sizing
One context.duo that knows if the phone is folded, open, sideways or sharing the screen.
Widgets that lay out, navigate and keep state the way iOS 27 does on Apple's first foldable.
Live hinge angle, posture and fold on iOS and Android. Works on iOS, Android, web and desktop, in English, Arabic and every other language.
واجهة وحدة context.duo تعرف إذا الموبايل مطبوگ لو مفتوح لو نص مفتوح لو تطبيق ثاني يتگاسم وياك الشاشة.
تقرا زاوية المفصل والوضعية لايف على iOS وAndroid، وتشتغل على iOS وAndroid والويب والديسكتوب، بالعربي والإنگليزي.
Built by Mohammad Othman (Abojawdat)
▶ Try the live demo · جرّب العرض الحي
A 3D iPhone Duo you fold with your hand, in your browser.
English
New in 2.0: a live hinge and Liquid Glass
A live hinge. On iOS 27.1 and Android foldables the package reads the real hinge: the angle, the posture and Apple's 40 pt fold. Wrap your app once in DuoHardwareScope and every layout below follows it. How it works.
Liquid Glass, off by default. Pass glass: true to DuoNavigationScaffold and the side rail, or the bottom bar as a floating capsule like iOS 26's tab bar, sits on Apple's real UIGlassEffect on iOS 26+, with the page flowing on beneath the glass, blurred. Elsewhere it gets the same look drawn in Flutter. Without the flag nothing changes.
Try it in 30 seconds
▶ Open the live demo in your browser. Nothing to install: a 3D iPhone Duo you fold with your hand.
- Drag the handle on the edge of the phone, or the Hinge slider. It bends from flat, through the book posture, to shut on its cover screen.
- Play a song, then fold it: it keeps playing, and the list slides in or out around the fold.
- Press Turn it for the tabletop posture, Split View to share the screen, or swap in an Android fold, an iPhone or an iPad.
Or run the same app yourself. No iPhone Duo needed: any phone, emulator, simulator or desktop works.
dart pub unpack duo_dynamic_sizing
cd duo_dynamic_sizing-*/example
flutter run
Using it in your own app instead? flutter pub add duo_dynamic_sizing, then see the minimal example.
See it in action
Every clip below is the real example app running this package, rendered frame by frame. Nothing is mocked up.
1. Fold and unfold
What happens: closed, the player fills the 466 pt outer screen. Open the phone and the list slides back in beside the same track, still playing from the same second.
Why: Apple treats continuity across a fold as a requirement. People fold in the middle of reading and expect to land exactly where they were. Nothing here rebuilds from scratch: the package moves the same widget element to its new place.
Code: DuoListDetail inside DuoNavigationScaffold, nothing else.
2. Split View
What happens: a second app joins. Yours shrinks live from 951 to 475 pt. The moment it becomes a Split View slice, its rail jumps from the island side to its own outer edge, and the list collapses to one pane.
Why: that's where iOS 27 puts the bars of the left-hand app. Every intermediate width is laid out for real, so there's no snapping between a few hand-picked sizes.
Code: nothing extra. context.duo.isSplit tells you when it happens.
3. Half open: tabletop and book
What happens: on a table, the photo jumps above the fold and its details drop to the bottom half. Held like a book, the details slide to the trailing half.
Why: a bent screen has two surfaces. Content that straddles the crease is hard to read and hard to tap, and Apple's guidance says the same.
Code: DuoSplit for the two halves, DuoAvoidFold for floating controls, context.duo.posture to decide.
4. Rotation
What happens: closed and turned sideways, the island moves to the top and the rail stays on the side. Open and turned upright, the rail becomes a bottom bar, the one pose where iOS keeps horizontal bars.
Why: the Duo's inner screen ignores orientation locks, so your layout has to follow the window, not the device orientation.
Code: DuoNavigationScaffold decides with context.duo.prefersRail.
5. Resizable windows
What happens: a desktop window is dragged from 360 to 1240 pt. The mode, the number of panes and the navigation all update on every frame.
Why: the same rules that handle the Duo handle iPad Stage Manager, web browsers and desktop windows. One set of layout decisions covers every screen.
Code: the same app, with no platform checks.
6. English and Arabic
What happens: the app switches to Arabic. The list moves to the right and the detail to the left, while the split stays exactly on the physical fold.
Why: reading direction decides where the primary pane goes; the hinge decides where the split goes. Mixing those two up would put content on the crease.
Code: automatic from your app's Directionality. Override with textDirection: and railSide:.
7. Media fit
What happens: the same 16:9 photo cycles through contain (bars), cover (cropped) and smart, which crops only when it loses less than 15%.
Why: the inner screen is √2 shaped, so 16:9 video leaves 134 pt of black bars. Sometimes a small crop is the better trade, and sometimes it isn't.
Code: DuoMedia(aspectRatio: 16 / 9, child: player).
iPhone Duo, every pose
| Pose | Window (pt) | What your app gets |
|---|---|---|
| Closed | 466 × 678 | One pane. The rail sits on the right, next to the vertical Dynamic Island. |
| Closed, sideways | 678 × 466 | One pane in landscape. |
| Open | 951 × 669 | Two panes, split exactly at the fold. |
| Open, upright | 669 × 951 | Two panes and a bottom bar. |
| Split View | about 475 × 669 | One pane per app, each with its rail on its own outer edge. |
And every other screen
On Android foldables the package reads the real fold from MediaQuery.displayFeatures and the live hinge angle from the hinge sensor. Phones, tablets, web and desktop get the same widgets, driven by window size.
Install
flutter pub add duo_dynamic_sizing
Needs Flutter 3.41+ (Dart 3.11+) and iOS 15+. No permissions. For live hinge data, wrap your app once in DuoHardwareScope (see Live hinge data); everything else works without it.
Minimal example
A complete app in about 40 lines: a mail list that becomes list + detail when there's room, with the right navigation on every screen. It's example/lib/minimal.dart; run it with flutter run -t lib/minimal.dart.
import 'package:duo_dynamic_sizing/duo_dynamic_sizing.dart';
import 'package:flutter/material.dart';
void main() =>
runApp(const DuoHardwareScope(child: MaterialApp(home: Inbox())));
class Inbox extends StatefulWidget {
const Inbox({super.key});
@override
State<Inbox> createState() => _InboxState();
}
class _InboxState extends State<Inbox> {
int tab = 0;
int? open;
@override
Widget build(BuildContext context) {
return DuoNavigationScaffold(
appBar: AppBar(title: Text(context.duo.mode.name)),
selectedIndex: tab,
onDestinationSelected: (i) => setState(() => tab = i),
destinations: const [
DuoDestination(icon: Icon(Icons.inbox), label: 'Inbox'),
DuoDestination(icon: Icon(Icons.star), label: 'Starred'),
],
body: DuoListDetail<int>(
selected: open,
onClose: () => setState(() => open = null),
empty: (_) => const Center(child: Text('Pick a message')),
list: (_) => ListView.builder(
itemCount: 30,
itemBuilder: (_, i) => ListTile(
title: Text('Message $i'),
selected: i == open,
onTap: () => setState(() => open = i),
),
),
detail: (_, i) => Center(child: Text('Message $i')),
),
);
}
}
The same code gives you all of this:
| Screen | Result |
|---|---|
| iPhone Duo closed | the list, and the message full screen once tapped; rail by the island |
| iPhone Duo open | list and message side by side, split at the fold |
| Split View | one pane, rail on the app's outer edge |
| Android foldable half open | panes split exactly at the hinge |
| iPhone | one pane, bottom bar |
| iPad, desktop, web | list and message side by side, side rail |
| Arabic or Hebrew app | everything mirrored, still split at the fold |
Reading the window yourself works anywhere:
final duo = context.duo; // rebuilds on fold, rotation and Split View
if (duo.isExpanded) { /* two panes */ }
duo.mode; // DuoMode.openLandscape
duo.safe.right; // 59, the Dynamic Island side
duo.gridColumns(160); // even across the fold
Why it's built this way
The short version is below. The full reasoning, with the alternatives we rejected, is in DESIGN.md (العربية).
- Sizes never scale. Both Duo screens have the same physical point size, about 153 points per inch. Width-scaled 16 pt text would jump to 33 pt when you unfold, so layouts gain panes, not bigger widgets.
- Everything comes from
MediaQueryand the hinge. That makes it reactive, testable and simulator-friendly, and it only rebuilds when the window or the posture really changes, never on every degree of the angle. - Native code only where the data lives. A small Swift file reads Apple's hinge, fold and camera APIs, looked up at runtime so it builds on any Xcode, and a small Java file reads Android's hinge sensor. Web and desktop stay pure Dart.
- The iPhone Duo is detected by its exact screen sizes at 3x on iOS, so iPads, Android phones and desktop windows are never mistaken for it.
- State survives every fold because the package moves widgets instead of rebuilding them.
- Tested hard: 141 unit, widget, edge-case and golden tests, plus integration tests on macOS, the iOS simulator, a Pixel Fold emulator and a real Honor foldable, including a live fold from flat to shut and back.
What's inside
context.duo |
Mode, open or closed, panes, fold, posture, per-side safe areas, margins, grid columns. |
DuoLayout + DuoKeep |
Separate closed and open layouts that keep shared widgets' state and scroll position. |
DuoSplit |
Two panes aligned to the fold or hinge, one pane when there's no room. Flutter's take on Apple's split ArrangementView. |
DuoListDetail |
Mail-style list and detail, with system back and fold continuity handled. |
DuoNavigationScaffold |
Bottom bar or side rail, placed where iOS 27 puts its vertical bars. railSide picks the side for LTR and RTL apps. |
DuoAvoidFold |
Keeps controls off a half-open fold: trailing half in book posture, bottom half on a table. |
DuoMedia |
Fits 16:9 video to the √2 screen, cropping only when little is lost. |
DuoHardwareScope + DuoHardware |
Live hinge angle, posture, the 40 pt fold and camera regions, size classes and the vertical bar edge, from iOS 27.1 and Android. |
DuoGlass |
Real Liquid Glass on iOS 26+, the same look drawn in Flutter elsewhere. DuoNavigationScaffold(glass: true) puts the rail, or a floating bottom bar, on it, with the page flowing on beneath. Off by default. |
DuoSimulator + DuoPose |
Every iPhone Duo pose, half open too, Android folds, iPhone and iPad, on any device and in widget tests. |
DuoDebugOverlay |
Live readout of mode, size, safe area, fold, posture and angle. |
context.duo
| Member | Type | What it tells you |
|---|---|---|
mode |
DuoMode |
closedPortrait, closedLandscape, openLandscape, openPortrait, splitView, tablet, desktop |
isIphoneDuo |
bool |
The window matches an iPhone Duo screen. |
isOpen, isClosed, isSplit |
bool |
Device state. |
isExpanded, columns |
bool, int |
Room for two panes (600 × 480 and up). Drive layout off these. |
fold, foldDirection |
Rect?, Axis? |
Where the fold runs, in window coordinates. |
isSeparating, posture |
bool, DuoPosture |
Half-open fold or hinge: flat, book, tabletop, closed. |
hardware |
DuoHardware |
Hinge status, angle, fold and camera regions, size classes, bar edge. Needs DuoHardwareScope. |
cameras, cameraInsets |
List<Rect>, EdgeInsets |
Cameras in use, and the padding that keeps content clear of them. |
safe, symmetricSafe |
EdgeInsets |
Per-side insets, or the mirrored maximum for centred content. |
margin |
double |
16 on compact widths, 24 otherwise (Material 3). |
prefersRail |
bool |
Side rail instead of a bottom bar. |
gridColumns(minWidth) |
int |
Columns that fit, kept even across a vertical fold. |
Change the thresholds for a subtree with DuoScope(expandedWidth: 700, child: ...).
Live hinge data
Wrap the app once, above MaterialApp:
void main() => runApp(const DuoHardwareScope(child: MyApp()));
From then on context.duo knows the real posture, and the fold is Apple's own 40 pt region while the phone is half open. Your layouts don't change: DuoSplit keeps both panes off the fold, DuoAvoidFold moves controls to one half, and the rail sits on the side iOS reports for its vertical bar.
final duo = context.duo;
duo.posture; // DuoPosture.book, from the hinge
duo.fold; // Rect(455.5, 0, 40, 669) while half open
duo.cameraInsets; // padding that clears the inner camera while it's in use
DuoHardware.angleOf(context); // live angle, rebuilds only this widget
DuoHardware.angleStream.listen((degrees) {}); // for per-frame effects
Layout readers rebuild on posture, fold, camera and size class changes, never on every degree of the angle. That follows Apple's guidance: drive layout from posture, and effects from the angle.
| Platform | What you get |
|---|---|
| iOS 27.1 (iPhone Duo) | Angle, hinge status, fold and camera regions, size classes, vertical bar edge |
| iOS before 27.1 | Size classes. Everything else stays empty, and nothing breaks |
| Android foldables | Angle and status from the hinge sensor. Posture and fold come from Android itself |
| Web, desktop, other phones | Nothing, DuoHardware.none |
Dialogs and sheets. Flutter squeezes every dialog, bottom sheet and picker onto one side of a half-open fold it finds in MediaQuery.displayFeatures. So by default the scope doesn't write there, and your dialogs keep their full width. Opt in with bridge:
bridge |
Effect |
|---|---|
DuoBridge.none (default) |
Nothing published. Framework behaviour unchanged. |
DuoBridge.cameras |
Cameras in use only, as cutouts. Surfaces keep their width. |
DuoBridge.all |
The fold too, only while half open. Dialogs and sheets move to one half. |
The fold is never published while the phone is flat or closed, or when it has no width, and the scope steps aside if Flutter starts reporting folds on iOS itself (flutter#192515).
In tests, pin the data instead of reading a device: DuoHardwareScope(hardware: DuoHardware(...), child: ...), or give a DuoPose a hardware. If something looks off on a real device, DuoHardware.describeNative() returns what the running OS actually exposes.
Widgets
Two layouts, one state. DuoKeep moves the same element between layouts instead of rebuilding it:
DuoLayout(
closed: (_) => DuoKeep(id: 'feed', child: const Feed()),
open: (_) => Row(children: [
Expanded(child: DuoKeep(id: 'feed', child: const Feed())),
const Expanded(child: Trends()),
]),
)
Panes that follow the fold:
DuoSplit(primary: const Player(), secondary: const UpNext())
Controls that dodge the crease:
Stack(children: [
const VideoPlayer(),
const DuoAvoidFold(
child: Align(alignment: Alignment.bottomCenter, child: Controls()),
),
])
Video that fits the screen:
DuoMedia(aspectRatio: 16 / 9, child: player) // smart: crops up to 15%, else letterboxes
Liquid Glass:
DuoNavigationScaffold(
glass: true, // off by default. real Liquid Glass on iOS 26+
background: const Mood(), // optional, runs under the body and the rail
...
)
DuoGlass(child: toolbar) // any capsule, follows your app's light or dark theme
With glass on, the side rail sits on glass, or the bottom bar floats as a glass capsule like iOS 26's tab bar, and the page flows on beneath it, blurred, the way Apple's background extension works. The page keeps its layout, so nothing hides under the glass. background adds your own layer under everything, like a gradient or a hero image.
RTL and LTR
Everything follows your app's Directionality, so Arabic, Hebrew and Persian apps get mirrored layouts with no extra code. When panes swap sides, the split still lands exactly on the physical fold.
| LTR app | RTL app | |
|---|---|---|
DuoSplit, DuoListDetail |
Primary (list) on the left | Primary (list) on the right |
DuoAvoidFold, book posture |
Right half | Left half |
DuoSplit(ratio: .3) |
Primary takes 30% on the left | Primary takes 30% on the right |
You choose where the navigation rail goes with railSide:
railSide |
iPhone Duo | Everywhere else |
|---|---|---|
auto (default) |
The side iOS reports for its vertical bar, else right next to the Dynamic Island, in every language | Start side: left in LTR, right in RTL |
start |
Start side | Start side |
end |
End side | End side |
left, right |
Always that side | Always that side |
DuoNavigationScaffold(railSide: DuoRailSide.start, ...) // follow the reading direction everywhere
DuoListDetail(textDirection: TextDirection.ltr, ...) // pin one layout, whatever the app uses
Test every pose without a device
testWidgets('unfolding keeps the draft', (tester) async {
await tester.pumpWidget(
const DuoSimulator(pose: DuoPose.closedPortrait, child: MyApp()),
);
await tester.enterText(find.byType(TextField), 'draft');
await tester.pumpWidget(
const DuoSimulator(pose: DuoPose.openLandscape, child: MyApp()),
);
expect(find.text('draft'), findsOneWidget);
});
Built-in poses: closedPortrait, closedLandscape, openLandscape, openPortrait, splitLeft, splitRight, halfOpenBook, halfOpenTabletop, foldableBook, foldableTabletop, iPhone, iPad. The two half-open Duo poses carry the hinge data iOS reports, 40 pt fold included. You can also build your own with DuoPose(...).
To see the live values while you develop:
MaterialApp(
builder: (context, child) =>
DuoDebugOverlay(enabled: kDebugMode, child: child!),
)
Shipping on the iPhone Duo
- Build with the iOS 27.1 SDK (Xcode 27.1). Apps built with older SDKs run letterboxed on the inner screen, and the fold and camera regions only exist from 27.1. The package itself builds on any Xcode.
- Use iOS 15 or newer as the deployment target. Xcode 27.1 rejects anything lower, including in pods. If Flutter's own pods still say 13.0, set
IPHONEOS_DEPLOYMENT_TARGET = '15.0'for every target in your Podfile'spost_install. - Adopt the UIScene lifecycle. Xcode 27 builds crash at launch without it. Flutter 3.41+ migrates an unmodified
AppDelegatefor you; see Flutter's UIScene guide. - Don't count on orientation locks. The inner screen ignores
SystemChrome.setPreferredOrientations. - Never cache sizes in
State. ReadMediaQueryorcontext.duoinbuild. - Treat left and right insets separately. The Dynamic Island runs down one side.
- Pause media on
paused, notinactive. In Split View your app stays visible whileinactive. - Keep taps off the fold when half open. Use
DuoAvoidFoldandDuoSplit.
The iPhone Duo at a glance
| Outer screen | Inner screen | |
|---|---|---|
| Diagonal | 5.4″ | 7.6″ |
| Panel | 1398 × 2034 px, 460 ppi | 1878 × 2670 px, 430 ppi |
| Points (what Flutter sees) | 466 × 678 @3x | 669 × 951 @3x, drawn at 2007 × 2853 then scaled to the panel |
| Size class | compact | regular × regular |
Apple announced it on 9 September 2026, and it ships on 23 October 2026 with iOS 27.1. It measures 164.6 × 117.8 mm open, 84.1 × 117.8 mm closed, and weighs 254 g.
Limitations
- Live hinge data needs
DuoHardwareScopeand, on iOS, the 27.1 runtime. Without it,postureon the Duo isclosedorunknownand the fold is the known physical centre line. The iOS side is written against Apple's iOS 27.1 headers and looks every API up at runtime, so a missing or changed API turns the feature off instead of crashing. It's tested on iOS 26 simulators, macOS, a Pixel Fold emulator and a real Honor foldable; if a real Duo reports something odd,DuoHardware.describeNative()shows what the OS exposes. - On Android the angle can start empty. After the phone wakes, Android may have no reading until the hinge first moves. Posture still comes from Android's own fold data.
- Simulator safe areas are estimates. Apple hasn't published the Duo's inset values, so
DuoPoseuses 59 pt and 21 pt. Real devices always use the live values. - The Split View side is inferred from which side has the Dynamic Island inset.
- UIKit's own toolbars aren't bridged.
DuoNavigationScaffoldplaces a Material rail on the side iOS reports for its vertical bar, andglass: truedraws it on real Liquid Glass.
Example
No clone needed, this pulls the package with its example from pub.dev:
dart pub unpack duo_dynamic_sizing
cd duo_dynamic_sizing-*/example
flutter run # the 3D showcase, on any phone, emulator or desktop
flutter run -t lib/minimal.dart # the 40-line app above
It is the same app as the live demo: a foldable drawn in 3D, with a music player, a photo gallery and a live readout of context.duo running inside it, including the hinge status, angle, cameras and bar edge. Drag the handle or the Hinge slider to fold it shut or open, Turn it to rotate it, Split View to share the screen, Play all to watch every posture by itself, or swap in an Android fold, an iPhone or an iPad. Guides paints what the system covers in red, like the Duo's Dynamic Island strip, and the fold in blue. This device drops the pretend phone and uses your actual screen (a Pixel Fold emulator reports a real hinge). The Every pose tab lists each posture, and Use it has the install line and a starter.
Contributing
Bug reports and pull requests are welcome in the issue tracker. If something looks off on a real iPhone Duo, a screenshot with DuoDebugOverlay turned on helps a lot.
Author
Built and maintained by Mohammad Othman (@Abojawdat).
- Profile: github.com/Abojawdat
- Source: github.com/Abojawdat/iPhone-Duo-package-
- Issues: report a bug or ask for a feature
If the package helps you, a star on the repo is the best way to say thanks.
العربية
الجديد بـ 2.0: مفصل لايف وLiquid Glass
مفصل لايف. على iOS 27.1 وموبايلات Android اللي تنطبگ، الحزمة تقرا المفصل الحقيقي: الزاوية والوضعية وخط الطبگة مال Apple (40 نقطة). لف تطبيقك مرة وحدة بـ DuoHardwareScope وكل التخطيطات تمشي وياه.
Liquid Glass، مطفي افتراضياً. مرر glass: true لـ DuoNavigationScaffold والشريط الجانبي، أو الشريط الجوة كحبة طايفة مثل iOS 26، يگعد على UIGlassEffect الحقيقي مال Apple على iOS 26 وأحدث، والصفحة تكمل تحت الزجاج مضبّبة. وبلا هالخيار ما يتغير شي.
جرّبها بـ 30 ثانية
▶ افتح العرض الحي بالمتصفح، بلا تنصيب: iPhone Duo ثلاثي الأبعاد تطبّگه بإيدك. اسحب المقبض أو شريط Hinge، شغّل أغنية وطبّگه: تبقى تشتغل.
أو شغّله عندك، وما تحتاج iPhone Duo، أي موبايل أو محاكي أو كمبيوتر يمشي:
dart pub unpack duo_dynamic_sizing
cd duo_dynamic_sizing-*/example
flutter run
دوس Turn it حتى تدوّره، أو Split View حتى تتقاسم الشاشة، أو بدّله بأندرويد أو iPhone أو iPad.
شوفها شلون تشتغل
كل مقطع جوة هو التطبيق التجريبي الحقيقي يشتغل بهاي الحزمة، مرسوم فريم ورا فريم. ماكو شي هنا تصميم وهمي.
١. تطبّگه وتفتحه
شنو يصير: وهو مطبوگ المشغّل يملّي الشاشة. افتحه وترجع القائمة يم نفس الأغنية، وهي بعدها تشتغل من نفس الثانية.
ليش: المستخدم يطبّگ الموبايل وهو يقرا، ويريد يرجع لنفس المكان.
الكود: DuoListDetail جوة DuoNavigationScaffold، وبس.
٢. لمن تطبيق ثاني يتگاسم وياك الشاشة (Split View)
شنو يصير: يدخل تطبيق ثاني فيصغر تطبيقك من 951 لـ 475 نقطة، شريط التنقل ينتقل للحافة البرّانية والقائمة تصير لوحة وحدة.
ليش: هنا iOS 27 يحط أشرطة التطبيق اليسار.
الكود: ولا شي زيادة. context.duo.isSplit تگلك شوكت يصير هالشي.
٣. نص مفتوح: گاعد عالميز ومثل الكتاب
شنو يصير: عالميز الصورة تصعد فوگ خط الطبگة وتفاصيلها تنزل جوته. ومثل الكتاب التفاصيل تروح للنص الثاني.
ليش: الشي اللي فوگ خط الطبگة صعب تقراه وتدوس عليه.
الكود: DuoSplit للنصين، وDuoAvoidFold للأزرار الطايفة، وcontext.duo.posture حتى تقرر.
٤. تگلب الموبايل
شنو يصير: المطبوگ بالعرض يخلي شريط التنقل عالجنب، والمفتوح بالطول يحوله لشريط التنقل الجوة.
ليش: الشاشة الداخلية ما تهتم لقفل الاتجاه، فالتخطيط يمشي ويه الشباك.
الكود: DuoNavigationScaffold يقرر هالشي من context.duo.prefersRail.
٥. تكبّر وتصغّر الشباك
شنو يصير: تسحب شباك عالديسكتوب من 360 لـ 1240 نقطة، والوضع وعدد اللوحات ونوع التنقل يتحدثون بكل فريم.
ليش: نفس القواعد تمشي على Duo وiPad والويب والديسكتوب.
الكود: نفس التطبيق، بلا ولا فحص للمنصة.
٦. عربي وإنگليزي
شنو يصير: التطبيق يتحول عربي، القائمة تروح لليمين والتفاصيل لليسار، بس التقسيم يبقى بالضبط على خط الطبگة الحقيقي.
ليش: اتجاه القراية يحدد وين اللوحة الأساسية، والمفصل يحدد وين التقسيم. لو خلطتهم المحتوى يوگع فوگ خط الطبگة.
الكود: أوتوماتيك حسب Directionality بتطبيقك. وتگدر تغيره بـ textDirection: وrailSide:.
٧. الصورة شلون تلبس الشاشة
شنو يصير: نفس صورة الـ 16:9 تتنقل بين contain (أشرطة سودة)، وcover (يگص)، وsmart اللي ما يگص إلا إذا اللي يروح أقل من 15%.
ليش: الشاشة الداخلية شكلها √2، ففيديو 16:9 يخلي أشرطة سودة طولها 134 نقطة. مرات گصة صغيرة أحسن، ومرات لا.
الكود: DuoMedia(aspectRatio: 16 / 9, child: player).
iPhone Duo بكل وضعياته
| الوضعية | الشباك (نقطة) | شنو ياخذ تطبيقك |
|---|---|---|
| مطبوگ | 466 × 678 | لوحة وحدة، والشريط الجانبي عاليمين يم الجزيرة الديناميكية العمودية. |
| مطبوگ بالعرض | 678 × 466 | لوحة وحدة بالعرض. |
| مفتوح | 951 × 669 | لوحتين يم بعض، مگسومة بالضبط على خط الطبگة. |
| مفتوح بالطول | 669 × 951 | لوحتين وشريط التنقل الجوة. |
| Split View | تقريباً 475 × 669 | لوحة وحدة لكل تطبيق، والشريط عالحافة البرّانية مال كل واحد. |
وكل شاشة ثانية
على موبايلات Android اللي تنطبگ، الحزمة تقرا خط الطبگة الحقيقي من MediaQuery.displayFeatures، وزاوية المفصل من حساس المفصل. والموبايلات والتابلتات والويب والديسكتوب ياخذون نفس عناصر الواجهة، حسب حجم الشباك.
التنصيب
flutter pub add duo_dynamic_sizing
تحتاج Flutter 3.41 أو أحدث (Dart 3.11 أو أحدث) وiOS 15 أو أحدث، وبلا صلاحيات. وحتى تاخذ بيانات المفصل لايف، لف تطبيقك مرة وحدة بـ DuoHardwareScope. باقي الأشياء تشتغل بدونه.
مثال سهل
تطبيق كامل بحدود 40 سطر: قائمة رسائل تصير قائمة وتفاصيل لمن تكبر المساحة، ويا نوع التنقل المناسب لكل شاشة. الملف هو example/lib/minimal.dart، وتگدر تشغله بـ flutter run -t lib/minimal.dart.
import 'package:duo_dynamic_sizing/duo_dynamic_sizing.dart';
import 'package:flutter/material.dart';
void main() =>
runApp(const DuoHardwareScope(child: MaterialApp(home: Inbox())));
class Inbox extends StatefulWidget {
const Inbox({super.key});
@override
State<Inbox> createState() => _InboxState();
}
class _InboxState extends State<Inbox> {
int tab = 0;
int? open;
@override
Widget build(BuildContext context) {
return DuoNavigationScaffold(
appBar: AppBar(title: Text(context.duo.mode.name)),
selectedIndex: tab,
onDestinationSelected: (i) => setState(() => tab = i),
destinations: const [
DuoDestination(icon: Icon(Icons.inbox), label: 'Inbox'),
DuoDestination(icon: Icon(Icons.star), label: 'Starred'),
],
body: DuoListDetail<int>(
selected: open,
onClose: () => setState(() => open = null),
empty: (_) => const Center(child: Text('Pick a message')),
list: (_) => ListView.builder(
itemCount: 30,
itemBuilder: (_, i) => ListTile(
title: Text('Message $i'),
selected: i == open,
onTap: () => setState(() => open = i),
),
),
detail: (_, i) => Center(child: Text('Message $i')),
),
);
}
}
نفس الكود ينطيك كل هذا:
| الشاشة | شنو يطلع |
|---|---|
| iPhone Duo مطبوگ | القائمة، وبعدين الرسالة بكل الشاشة لمن تدوس عليها، والشريط يم الجزيرة |
| iPhone Duo مفتوح | القائمة والرسالة يم بعض، مگسومة على خط الطبگة |
| Split View | لوحة وحدة، والشريط عالحافة البرّانية للتطبيق |
| Android نص مفتوح | اللوحتين مگسومة بالضبط عالمفصل |
| iPhone | لوحة وحدة وشريط التنقل الجوة |
| iPad والديسكتوب والويب | القائمة والرسالة يم بعض ويا الشريط الجانبي |
| تطبيق عربي أو عبري | كلشي معكوس، والتقسيم يبقى على خط الطبگة |
وتگدر تقرا معلومات الشباك بنفسك بأي مكان:
final duo = context.duo; // ينبني من جديد لمن تطبّگ أو تگلب أو بالـ Split View
if (duo.isExpanded) { /* لوحتين */ }
duo.mode; // DuoMode.openLandscape
duo.safe.right; // 59، صوب الجزيرة الديناميكية
duo.gridColumns(160); // عدد زوجي من الأعمدة حوالي خط الطبگة
ليش سويناها هيچ
هاي النسخة المختصرة. التفاصيل كلها، وشنو ذبينا وليش، موجودة بـ DESIGN.ar.md (English).
- الأحجام أبد ما تكبر. الشاشتين إلهن نفس كثافة النقطة، فالتخطيط ياخذ لوحات زيادة مو عناصر أكبر.
- كلشي يجي من
MediaQueryومن المفصل، فما ينبني من جديد إلا لمن يتغير الشباك أو الوضعية، مو ويه كل درجة من الزاوية. - كود أصلي بس وين لازم: ملف Swift صغير يقرا مفصل Apple، وملف Java يقرا حساس Android، والويب والديسكتوب Dart بس.
- نعرف الـ iPhone Duo من قياسات شاشته بالضبط على iOS، فما يتلخبط ويا iPad أو Android.
- ماكو شي يضيع لمن تطبّگ لأن الحزمة تنقل عناصر الواجهة بدل ما تبنيها من جديد.
- مجربة زين: 141 اختبار، واختبارات تكامل على macOS ومحاكي iOS ومحاكي Pixel Fold وموبايل Honor حقيقي، وبيها طبگة لايف من مفتوح لمسكّر.
شنو بالحزمة
context.duo |
الوضع وعدد اللوحات وخط الطبگة والوضعية والمناطق الآمنة. |
DuoLayout + DuoKeep |
تخطيط للمطبوگ وواحد للمفتوح، وماكو شي يضيع. |
DuoSplit |
لوحتين على خط الطبگة، أو لوحة وحدة إذا المساحة ضيجة. |
DuoListDetail |
قائمة وتفاصيل مثل تطبيق البريد. |
DuoNavigationScaffold |
شريط التنقل الجوة أو الشريط الجانبي، وين ما يحطه iOS 27. |
DuoAvoidFold |
يبعّد الأزرار عن خط الطبگة. |
DuoMedia |
يلبّس فيديو 16:9 عالشاشة بأقل گص. |
DuoHardwareScope + DuoHardware |
زاوية المفصل والوضعية وخط الطبگة (40 نقطة) والكاميرات، من iOS 27.1 وAndroid. |
DuoGlass |
Liquid Glass حقيقي على iOS 26 وأحدث، وتمويه يشبهه بباقي الأجهزة. |
DuoSimulator + DuoPose |
كل الوضعيات على أي جهاز وبالاختبارات. |
DuoDebugOverlay |
يراويك الوضع والحجم وخط الطبگة لايف. |
context.duo
جدول الخصائص كامل بـ القسم الإنگليزي. وإذا تريد تغير الحدود لجزء من الشجرة استخدم DuoScope(expandedWidth: 700, child: ...).
بيانات المفصل لايف
لف التطبيق مرة وحدة فوگ MaterialApp، ومن هناك context.duo يعرف الوضعية الحقيقية، وخط الطبگة يصير المنطقة اللي يحددها iOS (40 نقطة) لمن يكون نص مفتوح:
void main() => runApp(const DuoHardwareScope(child: MyApp()));
DuoHardware.angleOf(context); // الزاوية لايف
افتراضياً ما نكتب بـ displayFeatures، حتى تبقى الحوارات والـ sheets بعرضها الكامل. bridge: DuoBridge.all يخليها تروح لنص واحد لمن الموبايل نص مفتوح. الجداول الكاملة بـ القسم الإنگليزي.
عناصر الواجهة
تخطيطين وماكو شي يضيع. DuoKeep ينقل نفس العنصر بين التخطيطين بدل ما يبنيه من جديد:
DuoLayout(
closed: (_) => DuoKeep(id: 'feed', child: const Feed()),
open: (_) => Row(children: [
Expanded(child: DuoKeep(id: 'feed', child: const Feed())),
const Expanded(child: Trends()),
]),
)
لوحات تمشي ويه خط الطبگة:
DuoSplit(primary: const Player(), secondary: const UpNext())
أزرار تبعد عن خط الطبگة:
Stack(children: [
const VideoPlayer(),
const DuoAvoidFold(
child: Align(alignment: Alignment.bottomCenter, child: Controls()),
),
])
فيديو يلبس الشاشة:
DuoMedia(aspectRatio: 16 / 9, child: player) // smart: يگص لحد 15%، وإلا يحط أشرطة
من اليمين لليسار ومن اليسار لليمين
كلشي يمشي ويه Directionality بتطبيقك، فتطبيقات العربي والعبري والفارسي تاخذ تخطيطات معكوسة بلا ولا سطر زيادة. ولمن اللوحتين يتبادلن مكانهن، التقسيم يبقى بالضبط على خط الطبگة الحقيقي.
| تطبيق من اليسار لليمين | تطبيق من اليمين لليسار | |
|---|---|---|
DuoSplit وDuoListDetail |
اللوحة الأساسية (القائمة) عاليسار | اللوحة الأساسية (القائمة) عاليمين |
DuoAvoidFold مثل الكتاب |
النص اليمين | النص اليسار |
DuoSplit(ratio: .3) |
اللوحة الأساسية تاخذ 30% من اليسار | اللوحة الأساسية تاخذ 30% من اليمين |
وإنت تختار وين شريط التنقل بـ railSide:
railSide |
iPhone Duo | باقي الأجهزة |
|---|---|---|
auto (الافتراضي) |
الجهة اللي يحطها iOS لشريطه العمودي، وإلا اليمين يم الجزيرة الديناميكية، بكل اللغات | جهة البداية: اليسار بالإنگليزي، واليمين بالعربي |
start |
جهة البداية | جهة البداية |
end |
جهة النهاية | جهة النهاية |
left وright |
ديمة هاي الجهة | ديمة هاي الجهة |
DuoNavigationScaffold(railSide: DuoRailSide.start, ...) // امشي ويه اتجاه القراية بكل مكان
DuoListDetail(textDirection: TextDirection.ltr, ...) // ثبّت تخطيط واحد شنو ما چانت لغة التطبيق
جرّب كل الوضعيات بلا جهاز
testWidgets('unfolding keeps the draft', (tester) async {
await tester.pumpWidget(
const DuoSimulator(pose: DuoPose.closedPortrait, child: MyApp()),
);
await tester.enterText(find.byType(TextField), 'draft');
await tester.pumpWidget(
const DuoSimulator(pose: DuoPose.openLandscape, child: MyApp()),
);
expect(find.text('draft'), findsOneWidget);
});
الوضعيات الحاضرة: closedPortrait وclosedLandscape وopenLandscape وopenPortrait وsplitLeft وsplitRight وhalfOpenBook وhalfOpenTabletop وfoldableBook وfoldableTabletop وiPhone وiPad. وتگدر تسوي وضعياتك الخاصة بـ DuoPose(...).
وحتى تشوف القيم لايف وإنت تطوّر:
MaterialApp(
builder: (context, child) =>
DuoDebugOverlay(enabled: kDebugMode, child: child!),
)
گبل لا تنزّل تطبيقك على iPhone Duo
- ابني بـ Xcode 27.1، لو لا تطبيقك يشتغل بإطار ضيگ على الشاشة الداخلية.
- خلّي أقل نسخة iOS 15، لأن Xcode 27.1 يرفض الأقل.
- حوّل لـ UIScene (الدليل).
- لا تعتمد على قفل الاتجاه.
- اقرا
context.duoجوةbuildولا تخزن الأحجام. - الجزيرة الديناميكية بصوب واحد، فتعامل ويه كل حافة بروحها.
- وگّف الفيديو والصوت بـ
pausedموinactive. - بعّد الأزرار عن خط الطبگة.
iPhone Duo بلمحة
| الشاشة البرّانية | الشاشة الداخلية | |
|---|---|---|
| القطر | 5.4 بوصة | 7.6 بوصة |
| اللوحة | 1398 × 2034 بكسل، 460 بكسل في البوصة | 1878 × 2670 بكسل، 430 بكسل في البوصة |
| النقاط (اللي يشوفه Flutter) | 466 × 678 بكثافة 3x | 669 × 951 بكثافة 3x، تنرسم بدقة 2007 × 2853 وبعدين تصغر عاللوحة |
| فئة الحجم | ضيقة (compact) | عادية × عادية (regular) |
الشغلات اللي بعد ما تصير
- بيانات المفصل تحتاج
DuoHardwareScope، وعلى iOS تحتاج نظام 27.1. وبدونهاpostureياclosedياunknown. - على Android الزاوية ممكن تبدي فاضية لحد ما يتحرك المفصل أول مرة بعد ما يصحى الموبايل.
- المناطق الآمنة بالمحاكاة تقريبية (59 و21 نقطة). الأجهزة الحقيقية تستخدم القيم الحقيقية.
- جهة التطبيق بالـ Split View نستنتجها من الصوب اللي بي حافة الجزيرة الديناميكية.
- أشرطة UIKit نفسها ما منقولة. الشريط الجانبي ينحط بالجهة اللي يگولها iOS، و
glass: trueيرسمه على Liquid Glass حقيقي.
التطبيق التجريبي
dart pub unpack duo_dynamic_sizing
cd duo_dynamic_sizing-*/example
flutter run # the 3D showcase
flutter run -t lib/minimal.dart # the 40-line app
نفس العرض الحي: موبايل ينطبگ مرسوم ثلاثي الأبعاد، وبداخله مشغّل أغاني ومعرض صور وقراءة حية لـ context.duo. اسحب المقبض أو Hinge حتى تطبّگه أو تفتحه، Turn it حتى تدوّره، Split View حتى تتقاسم الشاشة، أو Play all حتى يمر بكل الوضعيات وحده. Guides يلوّن اللي يغطيه النظام بالأحمر وخط الطبگة بالأزرق، وThis device يستخدم شاشتك الحقيقية.
تريد تساعد؟
إذا لگيت غلط أو عندك تعديل، دزه على صفحة المشاكل وأهلاً بيك. وإذا شفت شي مو صح على iPhone Duo حقيقي، سكرين شوت ويا DuoDebugOverlay شغال يساعد هواية.
منو سواها
هاي الحزمة سواها ويتابعها محمد عثمان (@Abojawdat).
- الحساب: github.com/Abojawdat
- الكود: github.com/Abojawdat/iPhone-Duo-package-
- صفحة المشاكل: دز غلط أو اطلب ميزة
إذا فادتك الحزمة، انطي المستودع نجمة، هاي أحسن شكراً.
MIT © @Abojawdat · Source on GitHub
iPhone and iPhone Duo are trademarks of Apple Inc. This package isn't affiliated with or endorsed by Apple.
Libraries
- duo_dynamic_sizing
- Adaptive layout for the iPhone Duo and any other screen, with live hinge data on iOS and Android.